Tl;dr : The input was not checked for out of range values. By enforcing size change on the data type, the security vulnerability was addressed.
This is a write up for the Java Start Here challenge. This challenge was inspired by the Boeing 787, which would shut itself down every 248 days and hence would require resetting. The challenge is still available. If you wish to try it, you may want to skip reading the rest of this article.
The program
After cloning the repository, we build, run and test the program:
> make build
[i] Building the program
Sending build context to Docker daemon 125.4kB
Step 1/10 : FROM secdim/play-java:latest
# SNIP
BUILD SUCCESSFUL in 4s
4 actionable tasks: 4 executed
Removing intermediate container 42de0602a38e
---> 15648f4b2349
Step 10/10 : CMD ["java","-jar","/home/gradle/build/libs/gradle.jar"]
---> Running in e103019cd896
Removing intermediate container e103019cd896
---> b69bd1a22ace
Successfully built b69bd1a22ace
Successfully tagged secdim.lab.java:latest
Once the container is built, we can run the program:
> make run
[?] Enter an amount (e.g. 10, 1000):
100
[i] The amount does not require approval
Each challenge comes with two sets of tests, usability tests (test/java/appSpec.java
) that tests if the program runs as expected, security tests (test/java/appSecuritySpec.java
) that tests for the security weaknesses. We run usability tests by running make test
.
> make test
[i] Running functionality tests
Initialized native services in: /home/gradle/native
Initialized jansi services in: /home/gradle/native
To honour the JVM settings for this build a single-use Daemon process will be forked. See https://docs.gradle.org/7.4.2/userguide/gradle_daemon.html#sec:disabling_the_daemon.
# SNIP
BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed
[i] Well done! All functionality tests have been passed
Usability tests should always pass. If they fail, the challenge fails. Nobody cares about an unusable program. Security tests can be run make securitytest
> make securitytest
# SNIP
5 tests completed, 5 failed
Test report disabled, omitting generation of the HTML test report.
:securityTest (Thread[Execution worker for ':' Thread 10,5,main]) completed. Took 0.49 secs.
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':securityTest'.
> There were failing tests
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 4s
3 actionable tasks: 3 executed
As expected all security tests failed due to security weaknesses of the program. These security tests can guide us in fixing the security weaknesses.
Our objective is to pass security tests while not failing usability tests.
The vulnerable code
The following code snippet shows the vulnerable part of the program.
public boolean approval(String value) {
int amount = Integer.parseInt(value) + surcharge;
if(amount >= threshold) {
return true;
}
return false;
}
You can see that the input of the function is of type String. The Integer.parseInt()
parses the String to Integer. Later anther integer value, i.e. surchage
is added. The final result is stored into a integer type, amount
This arithmetic pattern is susceptible to a variety of weaknesses
- Null value
- A negative number (negative amount is semantically wrong)
- Integer overflow vulnerability (read this great article to learn why this vulnerability happens.)
Security tests (test/java/appSecuritySpec.java
) for this challenge explicitly check for the abovementioned weaknesses. For example, the following test expects the program to return ArithmeticExpection
when input is beyond Int32 range ( -2147483648 < int32 <= 2147483647
)
@Test
public void amount_bigger_than_IntMax_throw_exception() {
assertThrows(ArithmeticException.class, () -> {
Main app = new Main();
boolean res = app.approval("2147483648");
});
}
The solution
After reviewing security tests, we found the following exception handling controls should be implemented:
- NullPointerException: when the input value is null
- IllegalArgumentException: when the input value is negative
- ArithmeticException: when the input value exceeds the range of an int data type
Given the requirements, we implement our solution as the following:
public boolean approval(String value) {
if(value == null || value.trim().length() == 0 || value.isEmpty()) {
throw new NullPointerException();
}
BigInteger big_value = new BigInteger(value);
if (big_value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) <= 0) {
throw new ArithmeticException("Underflow");
}
if(big_value.compareTo(BigInteger.valueOf(0)) <= 0) {
throw new IllegalArgumentException();
}
big_value = big_value.add(BigInteger.valueOf(surcharge));
if (big_value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) >= 0) {
throw new ArithmeticException("Overflow");
}
return big_value.compareTo(BigInteger.valueOf(threshold)) >= 0;
}
It is trivial to handle most exceptions. However, we need a technique to address integer overflow. The int data type can contain a maximum value of 2147483647 and a minimum value of -2147483647. Any value exceeding or less than the given value will result in an Integer Overflow, therefore a safeguard against such a case is required.
From Java Secure Programming Fundamental I, we learn the following technique to handle over the range values:
- Precondition testing: check the input before each arithmetic operation. If safe, proceed with the operation, otherwise throw an exception.
- Upcasting: use a larger datatype to perform the arithmetic. Check for an overflow within a smaller datatype’s range before downcasting.
- BigInteger: use a datatype that makes use of dynamic memory allocation.
We picked the last technique: BigInteger big_value = new BigInteger(value);
:
- We first check if the value is beyond the range,
- if not we check if
value + surcharge
will go beyond the range. Otherwise, we throw exception.
There are many other ways to address this program weaknesses. This solution was the most explicit.
Lessons learnt
We have learnt how to security handle the following weaknesses using Precondition testing:
A simple but powerful security check is range
check. Make sure you apply a range check on all numeric data type, before any arithmetic operations and also length check on String data type. For more information see Java Secure Programming Fundamental II.
That’s all.
If you would like to do security programming challenges, every month we host an online community security programming game event. You can join us at SecGames or you can try one of the many public challenges hosted on SecDim Play .