Skip to content

Commit e25b3e4

Browse files
committed
Add detection for Recursion in Java
1 parent 34369b3 commit e25b3e4

File tree

16 files changed

+342
-2
lines changed

16 files changed

+342
-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 Blog: [Low-effort denial of service with recursion](https://blog.trailofbits.com/2024/05/16/TODO/)
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 Blog: <a href="https://blog.trailofbits.com/2024/05/16/TODO/">Low-effort denial of service with 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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @name Recursive functions
3+
* @id tob/java/recursion
4+
* @description Detects recursive calls
5+
* @kind 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+
15+
predicate isTestPackage(RefType referenceType) {
16+
referenceType.getPackage().getName().toLowerCase().matches("%test%") or
17+
referenceType.getPackage().getName().toLowerCase().matches("%benchmark%") or
18+
referenceType.getName().toLowerCase().matches("%test%")
19+
}
20+
21+
predicate isBefore(Method a, Method b) {
22+
a.getLocation().getStartLine() < b.getLocation().getStartLine()
23+
}
24+
25+
abstract class RecursiveCall extends MethodCall {
26+
Method m1;
27+
Method m2;
28+
Method m3;
29+
Method m4;
30+
31+
abstract string asString();
32+
33+
string toString(Method m) {
34+
result = m.getName() + "(" + m.getLocation().getStartLine() + ")" + " -> "
35+
}
36+
}
37+
38+
class RecursiveCallOrder1 extends RecursiveCall {
39+
RecursiveCallOrder1() {
40+
this.getMethod() = this.getEnclosingCallable() and
41+
m1 = this.getMethod()
42+
}
43+
44+
override string asString() { result = "Recursion(1): " + toString(m1) + m1.getName() }
45+
}
46+
47+
class RecursiveCallOrder2 extends RecursiveCall {
48+
//RecursiveCallOrder1 {
49+
RecursiveCallOrder2() {
50+
m1 = this.getMethod() and
51+
m2 = m1.getACallee() and
52+
m2.getACallee() = m1 and
53+
isBefore(m1, m2)
54+
}
55+
56+
override string asString() {
57+
result = "Recursion(2): " + toString(m1) + toString(m2) + m1.getName()
58+
}
59+
}
60+
61+
class RecursiveCallOrder3 extends RecursiveCall {
62+
RecursiveCallOrder3() {
63+
m1 = this.getMethod() and
64+
m2 = m1.getACallee() and
65+
m3 = m2.getACallee() and
66+
m3.getACallee() = m1 and
67+
isBefore(m1, m2) and
68+
isBefore(m1, m3)
69+
}
70+
71+
override string asString() {
72+
result = "Recursion(3): " + toString(m1) + toString(m2) + toString(m3) + m1.getName()
73+
}
74+
}
75+
76+
class RecursiveCallOrder4 extends RecursiveCall {
77+
RecursiveCallOrder4() {
78+
m1 = this.getMethod() and
79+
m2 = m1.getACallee() and
80+
m3 = m2.getACallee() and
81+
m4 = m3.getACallee() and
82+
m4.getACallee() = m1 and
83+
isBefore(m1, m2) and
84+
isBefore(m1, m3) and
85+
isBefore(m1, m4)
86+
}
87+
88+
override string asString() {
89+
result =
90+
"Recursion(4): " + toString(m1) + toString(m2) + toString(m3) + toString(m4) + m1.getName()
91+
}
92+
}
93+
94+
from RecursiveCall recursiveCall
95+
where not isTestPackage(recursiveCall.getMethod().getDeclaringType())
96+
select recursiveCall, "$@", recursiveCall, recursiveCall.asString()

0 commit comments

Comments
 (0)