Skip to content

Commit 134e302

Browse files
authored
Merge pull request #22119 from owen-mc/java/fix-tainted-path-pattern-sanitization
Java: fix `@Pattern` sanitization for `java/path-injection`
2 parents a16e19a + a812e4a commit 134e302

9 files changed

Lines changed: 65 additions & 19 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Regular expression checks via annotation with `@javax.validation.constraints.Pattern` are now recognized as sanitizers for `java/path-injection`.

java/ql/lib/semmle/code/java/Concepts.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ private module Frameworks {
2525
*
2626
* These are either method calls, which return `true` when there is a match, or
2727
* annotations, which are considered to match if they are present.
28+
*
29+
* To use a `RegexMatch` as a barrier or sanitizer, instantiate
30+
* `DataFlow::BarrierGuard`. Method-call matches act as ordinary control-flow
31+
* barrier guards, while annotation matches are treated as direct barriers,
32+
* since an annotation does not dominate the expression it constrains;
33+
* `DataFlow::BarrierGuard` handles both, so callers need not distinguish the two.
2834
*/
2935
class RegexMatch extends Expr instanceof RegexMatch::Range {
3036
/** Gets the expression for the regex being executed by this node. */

java/ql/lib/semmle/code/java/dataflow/internal/DataFlowUtil.qll

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,17 @@ module BarrierGuard<guardChecksSig/3 guardChecks> {
418418
}
419419

420420
/** Gets a node that is safely guarded by the given guard check. */
421-
Node getABarrierNode() { result = BarrierGuardValue<guardChecks0/3>::getABarrierNode() }
421+
Node getABarrierNode() {
422+
result = BarrierGuardValue<guardChecks0/3>::getABarrierNode()
423+
or
424+
// An annotation doesn't dominate the expression it constrains, so the
425+
// barrier-guard machinery above never yields a node for it; treat it as a
426+
// direct barrier instead. Annotations are always present, so we only
427+
// consider the `true` branch.
428+
exists(Guard g, Expr e |
429+
g instanceof Annotation and guardChecks(g, e, true) and result.asExpr() = e
430+
)
431+
}
422432
}
423433

424434
bindingset[this]

java/ql/lib/semmle/code/java/security/LogInjection.qll

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,6 @@ private predicate logInjectionSanitizer(Expr e) {
9090
target.getStringValue() = ["\n", "\r", "\\n", "\\r", "\\R"]
9191
)
9292
)
93-
or
94-
exists(RegexMatch rm, CompileTimeConstantExpr target |
95-
rm instanceof Annotation and
96-
e = rm.getASanitizedExpr() and
97-
target = rm.getRegex() and
98-
regexPreventsLogInjection(target.getStringValue(), true)
99-
)
10093
}
10194

10295
/**
@@ -113,7 +106,6 @@ private predicate logInjectionGuard(Guard g, Expr e, boolean branch) {
113106
or
114107
exists(RegexMatch rm, CompileTimeConstantExpr target |
115108
rm = g and
116-
not rm instanceof Annotation and
117109
target = rm.getRegex() and
118110
e = rm.getASanitizedExpr()
119111
|

java/ql/lib/semmle/code/java/security/PathSanitizer.qll

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ private import semmle.code.java.dataflow.SSA
1010
private import semmle.code.java.frameworks.kotlin.IO
1111
private import semmle.code.java.frameworks.kotlin.Text
1212
private import semmle.code.java.dataflow.Nullness
13+
private import semmle.code.java.security.Sanitizers
1314

1415
/** A sanitizer that protects against path injection vulnerabilities. */
1516
abstract class PathInjectionSanitizer extends DataFlow::Node { }
@@ -493,7 +494,8 @@ private predicate directoryCharactersGuard(Guard g, Expr e, boolean branch) {
493494
*/
494495
private class DirectoryCharactersSanitizer extends PathInjectionSanitizer {
495496
DirectoryCharactersSanitizer() {
496-
this.asExpr() instanceof ReplaceDirectoryCharactersSanitizer or
497+
this.asExpr() instanceof ReplaceDirectoryCharactersSanitizer
498+
or
497499
this = DataFlow::BarrierGuard<directoryCharactersGuard/3>::getABarrierNode()
498500
}
499501
}

java/ql/lib/semmle/code/java/security/Sanitizers.qll

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ class SimpleTypeSanitizer extends DataFlow::Node {
3737
*
3838
* This is overapproximate: we do not attempt to reason about the correctness of the regexp.
3939
*
40-
* Use this if you want to define a derived `DataFlow::BarrierGuard` without
41-
* make the type recursive. Otherwise use `RegexpCheckBarrier`.
40+
* This holds for both method-call and annotation regular-expression matches.
41+
* Method-call matches yield barrier nodes via ordinary control-flow dominance,
42+
* while annotation matches are treated as direct barriers by
43+
* `DataFlow::BarrierGuard`, since an annotation does not dominate the
44+
* expression it constrains.
4245
*/
4346
predicate regexpMatchGuardChecks(Guard guard, Expr e, boolean branch) {
44-
exists(RegexMatch rm | not rm instanceof Annotation |
47+
exists(RegexMatch rm |
4548
guard = rm and
4649
e = rm.getASanitizedExpr() and
4750
branch = true
@@ -56,11 +59,6 @@ predicate regexpMatchGuardChecks(Guard guard, Expr e, boolean branch) {
5659
class RegexpCheckBarrier extends DataFlow::Node {
5760
RegexpCheckBarrier() {
5861
this = DataFlow::BarrierGuard<regexpMatchGuardChecks/3>::getABarrierNode()
59-
or
60-
// Annotations don't fit into the model of barrier guards because the
61-
// annotation doesn't dominate the sanitized expression, so we instead
62-
// treat them as barriers directly.
63-
exists(RegexMatch rm | rm instanceof Annotation | this.asExpr() = rm.getString())
6462
}
6563
}
6664

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import java.io.FileInputStream;
2+
import java.io.IOException;
3+
import java.nio.charset.StandardCharsets;
4+
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
@RestController
11+
public class SanitizationTests2 {
12+
13+
@GetMapping("/product/vuln/path-injection")
14+
public ResponseEntity<String> vulnerablePathInjection(@RequestParam("path") String path) throws IOException { // $ Source[java/path-injection]
15+
// Intentionally vulnerable for SAST testing: user-controlled path reaches file read sink.
16+
try (FileInputStream stream = new FileInputStream(path)) { // $ Alert[java/path-injection]
17+
byte[] fileContents = stream.readNBytes(1024);
18+
return ResponseEntity.ok(new String(fileContents, StandardCharsets.UTF_8));
19+
}
20+
}
21+
22+
@GetMapping("/product/vuln/path-injection-fix")
23+
public ResponseEntity<String> vulnerablePathInjectionFix(@RequestParam("path")
24+
@javax.validation.constraints.Pattern(regexp = "[a-zA-Z0-9]*") String path) throws IOException {
25+
try (FileInputStream stream = new FileInputStream(path)) {
26+
byte[] fileContents = stream.readNBytes(1024);
27+
return ResponseEntity.ok(new String(fileContents, StandardCharsets.UTF_8));
28+
}
29+
}
30+
}

java/ql/test/query-tests/security/CWE-022/semmle/tests/TaintedPath.expected

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#select
2+
| SanitizationTests2.java:16:59:16:62 | path | SanitizationTests2.java:14:59:14:91 | path : String | SanitizationTests2.java:16:59:16:62 | path | This path depends on a $@. | SanitizationTests2.java:14:59:14:91 | path | user-provided value |
23
| TaintedPath.java:16:71:16:78 | filename | TaintedPath.java:13:58:13:78 | getInputStream(...) : InputStream | TaintedPath.java:16:71:16:78 | filename | This path depends on a $@. | TaintedPath.java:13:58:13:78 | getInputStream(...) | user-provided value |
34
| Test.java:37:52:37:68 | (...)... | Test.java:32:16:32:45 | getParameter(...) : String | Test.java:37:52:37:68 | (...)... | This path depends on a $@. | Test.java:32:16:32:45 | getParameter(...) | user-provided value |
45
| Test.java:39:32:39:48 | (...)... | Test.java:32:16:32:45 | getParameter(...) : String | Test.java:39:32:39:48 | (...)... | This path depends on a $@. | Test.java:32:16:32:45 | getParameter(...) | user-provided value |
@@ -77,6 +78,7 @@
7778
| Test.java:199:19:199:33 | (...)... | Test.java:32:16:32:45 | getParameter(...) : String | Test.java:199:19:199:33 | (...)... | This path depends on a $@. | Test.java:32:16:32:45 | getParameter(...) | user-provided value |
7879
| Test.java:204:20:204:36 | (...)... | Test.java:32:16:32:45 | getParameter(...) : String | Test.java:204:20:204:36 | (...)... | This path depends on a $@. | Test.java:32:16:32:45 | getParameter(...) | user-provided value |
7980
edges
81+
| SanitizationTests2.java:14:59:14:91 | path : String | SanitizationTests2.java:16:59:16:62 | path | provenance | Sink:MaD:23 |
8082
| TaintedPath.java:13:17:13:89 | new BufferedReader(...) : BufferedReader | TaintedPath.java:14:27:14:40 | filenameReader : BufferedReader | provenance | |
8183
| TaintedPath.java:13:36:13:88 | new InputStreamReader(...) : InputStreamReader | TaintedPath.java:13:17:13:89 | new BufferedReader(...) : BufferedReader | provenance | MaD:74 |
8284
| TaintedPath.java:13:58:13:78 | getInputStream(...) : InputStream | TaintedPath.java:13:36:13:88 | new InputStreamReader(...) : InputStreamReader | provenance | Src:MaD:72 MaD:76 |
@@ -312,6 +314,8 @@ models
312314
| 75 | Summary: java.io; BufferedReader; true; readLine; ; ; Argument[this]; ReturnValue; taint; manual |
313315
| 76 | Summary: java.io; InputStreamReader; false; InputStreamReader; ; ; Argument[0]; Argument[this]; taint; manual |
314316
nodes
317+
| SanitizationTests2.java:14:59:14:91 | path : String | semmle.label | path : String |
318+
| SanitizationTests2.java:16:59:16:62 | path | semmle.label | path |
315319
| TaintedPath.java:13:17:13:89 | new BufferedReader(...) : BufferedReader | semmle.label | new BufferedReader(...) : BufferedReader |
316320
| TaintedPath.java:13:36:13:88 | new InputStreamReader(...) : InputStreamReader | semmle.label | new InputStreamReader(...) : InputStreamReader |
317321
| TaintedPath.java:13:58:13:78 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/apache-commons-io-2.6:${testdir}/../../../../../stubs/cargo:${testdir}/../../../../../stubs/apache-ant-1.10.13:${testdir}/../../../../../stubs/stapler-1.263:${testdir}/../../../../../stubs/javax-servlet-2.5:${testdir}/../../../../../stubs/apache-commons-jelly-1.0.1:${testdir}/../../../../../stubs/apache-commons-fileupload-1.4:${testdir}/../../../../../stubs/saxon-xqj-9.x:${testdir}/../../../../../stubs/apache-commons-beanutils:${testdir}/../../../../../stubs/dom4j-2.1.1:${testdir}/../../../../../stubs/apache-commons-lang:${testdir}/../../../../../stubs/jaxen-1.2.0:${testdir}/../../../../../stubs/jmh-1.3.6:${testdir}/../../../../../stubs/springframework-5.8.x:${testdir}/../../../../../stubs/jaxws-api-2.0:${testdir}/../../../../../stubs/apache-cxf
1+
// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/javax-validation-constraints:${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/apache-commons-io-2.6:${testdir}/../../../../../stubs/cargo:${testdir}/../../../../../stubs/apache-ant-1.10.13:${testdir}/../../../../../stubs/stapler-1.263:${testdir}/../../../../../stubs/javax-servlet-2.5:${testdir}/../../../../../stubs/apache-commons-jelly-1.0.1:${testdir}/../../../../../stubs/apache-commons-fileupload-1.4:${testdir}/../../../../../stubs/saxon-xqj-9.x:${testdir}/../../../../../stubs/apache-commons-beanutils:${testdir}/../../../../../stubs/dom4j-2.1.1:${testdir}/../../../../../stubs/apache-commons-lang:${testdir}/../../../../../stubs/jaxen-1.2.0:${testdir}/../../../../../stubs/jmh-1.3.6:${testdir}/../../../../../stubs/springframework-5.8.x:${testdir}/../../../../../stubs/jaxws-api-2.0:${testdir}/../../../../../stubs/apache-cxf

0 commit comments

Comments
 (0)