Skip to content

Commit fd6e8cd

Browse files
authored
Add detection for Recursion in Java (#14)
1 parent 5876834 commit fd6e8cd

File tree

16 files changed

+649
-2
lines changed

16 files changed

+649
-2
lines changed

.codeqlmanifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"provide": [
33
"cpp/*/qlpack.yml",
4-
"go/*/qlpack.yml"
4+
"go/*/qlpack.yml",
5+
"java/*/qlpack.yml"
56
]
67
}

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ jobs:
1616
run: |
1717
${{ steps.init.outputs.codeql-path }} test run ./cpp/test/
1818
${{ steps.init.outputs.codeql-path }} test run ./go/test/
19+
${{ steps.init.outputs.codeql-path }} test run ./java/test/

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ codeql database analyze database.db --format=sarif-latest --output=./tob.sarif -
6666
|[Missing MinVersion in tls.Config](./go/src/docs/security/MissingMinVersionTLS/MissingMinVersionTLS.md)|This rule finds cases when you do not set the `tls.Config.MinVersion` explicitly for servers. By default version 1.0 is used, which is considered insecure. This rule does not mark explicitly set insecure versions|error|medium|
6767
|[Trim functions misuse](./go/src/docs/security/TrimMisuse/TrimMisuse.md)|Finds calls to `string.{Trim,TrimLeft,TrimRight}` with the 2nd argument not being a cutset but a continuous substring to be trimmed|error|low|
6868

69+
### Java-kotlin
70+
71+
#### Security
72+
73+
| Name | Description | Severity | Precision |
74+
| --- | ----------- | :----: | :--------: |
75+
|[Recursive functions](./java-kotlin/src/docs/security/Recursion/Recursion.md)|Detects recursive calls|warning|low|
76+
6977
## Query suites
7078

7179
CodeQL queries are grouped into "suites". To execute queries from a specific suit add its name after a colon: `trailofbits/cpp-queries:codeql-suites/tob-cpp-full.qls`.
@@ -89,7 +97,7 @@ echo "--search-path '$PWD/codeql-queries'" > "${HOME}/.config/codeql/config"
8997

9098
Check that CodeQL CLI detects the new qlpacks:
9199
```sh
92-
codeql resolve qlpacks | grep trailofbits
100+
codeql resolve packs | grep trailofbits
93101
```
94102

95103
#### Before committing
@@ -99,6 +107,7 @@ Run tests:
99107
cd codeql-queries
100108
codeql test run ./cpp/test
101109
codeql test run ./go/test
110+
codeql test run ./java/test
102111
```
103112

104113
Update dependencies:
@@ -115,4 +124,5 @@ Generate markdown query help files
115124
```sh
116125
codeql generate query-help ./cpp/src/ --format=markdown --output ./cpp/src/docs
117126
codeql generate query-help ./go/src/ --format=markdown --output ./go/src/docs
127+
codeql generate query-help ./java/src/ --format=markdown --output ./java/src/docs
118128
```

java/src/codeql-pack.lock.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
lockVersion: 1.0.0
3+
dependencies:
4+
codeql/dataflow:
5+
version: 1.1.5
6+
codeql/java-all:
7+
version: 4.2.0
8+
codeql/mad:
9+
version: 1.0.11
10+
codeql/rangeanalysis:
11+
version: 1.0.11
12+
codeql/regex:
13+
version: 1.0.11
14+
codeql/ssa:
15+
version: 1.0.11
16+
codeql/threat-models:
17+
version: 1.0.11
18+
codeql/tutorial:
19+
version: 1.0.11
20+
codeql/typeflow:
21+
version: 1.0.11
22+
codeql/typetracking:
23+
version: 1.0.11
24+
codeql/util:
25+
version: 1.0.11
26+
codeql/xml:
27+
version: 1.0.11
28+
compiled: false
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- description: Security queries for Java
2+
- queries: 'security'
3+
from: trailofbits/java-queries
4+
- exclude:
5+
tags contain: experimental
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- description: Queries for Java
2+
- queries: '.'
3+
from: trailofbits/java-queries
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Recursive functions
2+
Recursive functions are methods that call themselves either directly or indirectly through other functions. While recursion can be a powerful programming technique, unbounded recursion on user inputs can lead to stack overflow errors and program crashes, potentially enabling denial of service attacks. This query detects recursive patterns up to order 4.
3+
4+
5+
## Recommendation
6+
Review recursive functions and ensure that they are either: - Not processing user-controlled data - The data has been properly sanitized before recursing - The recursion has an explicit depth limit
7+
8+
Consider replacing recursion with iterative alternatives where possible.
9+
10+
11+
## Example
12+
13+
```java
14+
// From https://github.com/x-stream/xstream/blob/dfa1d35462fe84412ee72a9b0cf5b5c633086520/xstream/src/java/com/thoughtworks/xstream/io/binary/BinaryStreamReader.java#L165
15+
private Token readToken() {
16+
// ...
17+
try {
18+
final Token token = tokenFormatter.read(in);
19+
switch (token.getType()) {
20+
case Token.TYPE_MAP_ID_TO_VALUE: // 0x2
21+
idRegistry.put(token.getId(), token.getValue());
22+
return readToken(); // Next one please.
23+
default:
24+
return token;
25+
}
26+
} catch (final IOException e) {
27+
throw new StreamException(e);
28+
}
29+
// ...
30+
}
31+
```
32+
In this example, a binary stream reader processes tokens recursively.
33+
34+
For each new token \`0x2\`, the parser will create a new recursive call. If this stream is user-controlled, an attacker can generate too many stackframes and crash the application with a `StackOverflow` error.
35+
36+
37+
## References
38+
* Trail Of Bits white paper: [Input-Driven Recursion](https://resources.trailofbits.com/input-driven-recursion-white-paper)
39+
* CWE-674: [Uncontrolled Recursion](https://cwe.mitre.org/data/definitions/674.html)

java/src/qlpack.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
name: trailofbits/java-queries
3+
description: CodeQL queries for Java developed by Trail of Bits
4+
authors: Trail of Bits
5+
version: 0.0.1
6+
license: AGPL
7+
library: false
8+
extractor: java-kotlin
9+
dependencies:
10+
codeql/java-all: "*"
11+
suites: codeql-suites
12+
defaultSuiteFile: codeql-suites/tob-java-code-scanning.qls
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Recursive functions are methods that call themselves either directly or indirectly through other functions.
6+
While recursion can be a powerful programming technique, unbounded recursion on user inputs can lead
7+
to stack overflow errors and program crashes, potentially enabling denial of service attacks.
8+
9+
This query detects recursive patterns up to order 4.
10+
</p>
11+
12+
</overview>
13+
<recommendation>
14+
<p>
15+
Review recursive functions and ensure that they are either:
16+
- Not processing user-controlled data
17+
- The data has been properly sanitized before recursing
18+
- The recursion has an explicit depth limit
19+
</p>
20+
<p>
21+
Consider replacing recursion with iterative alternatives where possible.
22+
</p>
23+
</recommendation>
24+
<example>
25+
<sample src="RecursiveCall.java" />
26+
<p>In this example, a binary stream reader processes tokens recursively.</p>
27+
<p>For each new token `0x2`, the parser will create a new recursive call.
28+
If this stream is user-controlled, an attacker can generate too many stackframes
29+
and crash the application with a <code>StackOverflow</code> error.</p>
30+
</example>
31+
<references>
32+
<li>
33+
Trail Of Bits white paper: <a href="https://resources.trailofbits.com/input-driven-recursion-white-paper">Input-Driven Recursion</a>
34+
</li>
35+
<li>
36+
CWE-674: <a href="https://cwe.mitre.org/data/definitions/674.html">Uncontrolled Recursion</a>
37+
</li>
38+
</references>
39+
</qhelp>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @name Recursive functions
3+
* @id tob/java/unbounded-recursion
4+
* @description Detects possibly unbounded recursive calls
5+
* @kind path-problem
6+
* @tags security
7+
* @precision low
8+
* @problem.severity warning
9+
* @security-severity 3.0
10+
* @group security
11+
*/
12+
13+
import java
14+
import semmle.code.java.dataflow.DataFlow
15+
16+
predicate isTestPackage(RefType referenceType) {
17+
referenceType.getPackage().getName().toLowerCase().matches("%test%") or
18+
referenceType.getPackage().getName().toLowerCase().matches("%benchmark%") or
19+
referenceType.getName().toLowerCase().matches("%test%")
20+
}
21+
22+
class RecursionSource extends Method {
23+
RecursionSource() {
24+
not isTestPackage(this.getDeclaringType()) and
25+
this.calls+(this)
26+
}
27+
}
28+
29+
/**
30+
* Check if the Expr uses directly an argument of the enclosing function
31+
*/
32+
class ParameterOperation extends Expr {
33+
ParameterOperation() {
34+
(this instanceof BinaryExpr or this instanceof UnaryAssignExpr) and
35+
exists(VarAccess va | va.getVariable() = this.getEnclosingCallable().getAParameter() |
36+
this.getAChildExpr+() = va
37+
)
38+
}
39+
}
40+
41+
module RecursiveConfig implements DataFlow::StateConfigSig {
42+
class FlowState = Method;
43+
44+
predicate isSource(DataFlow::Node node, FlowState firstMethod) {
45+
node.asExpr().(MethodCall).getCallee() instanceof RecursionSource and
46+
firstMethod = node.asExpr().(MethodCall).getCallee()
47+
}
48+
49+
predicate isSink(DataFlow::Node node, FlowState firstMethod) {
50+
node.asExpr().(MethodCall).getCallee().calls(firstMethod) and
51+
firstMethod.calls+(node.asExpr().(MethodCall).getCaller())
52+
}
53+
54+
predicate isBarrier(DataFlow::Node node) {
55+
exists(MethodCall ma |
56+
ma = node.asExpr() and
57+
exists(Expr e | e = ma.getAnArgument() and e instanceof ParameterOperation)
58+
)
59+
}
60+
// /**
61+
// * Weird but useful deduplication logic
62+
// */
63+
// predicate isBarrierOut(DataFlow::Node node, FlowState state) {
64+
// node.asExpr().(MethodCall).getCallee().getName() > state.getName()
65+
// }
66+
}
67+
68+
module RecursiveFlow = DataFlow::GlobalWithState<RecursiveConfig>;
69+
70+
import RecursiveFlow::PathGraph
71+
72+
/*
73+
* TODO: This query could be improved with the following ideas:
74+
* - limit results to methods that take any user input
75+
* - do not return methods that have calls to self (or unbounded recursion) that are conditional
76+
* - gather and print whole call graph (list of calls from recursiveMethod to itself)
77+
*/
78+
79+
from RecursiveFlow::PathNode source, RecursiveFlow::PathNode sink
80+
where RecursiveFlow::flowPath(source, sink)
81+
select sink.getNode(), source, sink, "Found a recursion: "

0 commit comments

Comments
 (0)