Skip to content

Commit 72d7fe4

Browse files
committed
Refactor creating barriers from RegexMatch
It is better to have the logic in one place, to avoid encountering this bug again.
1 parent e5bd62d commit 72d7fe4

4 files changed

Lines changed: 55 additions & 35 deletions

File tree

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ 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, use the `RegexMatchBarrier`
30+
* module rather than instantiating `DataFlow::BarrierGuard` directly. An
31+
* annotation match does not dominate the expression it constrains, so it
32+
* cannot act as a control-flow barrier guard; `RegexMatchBarrier` handles
33+
* method-call matches as barrier guards and annotation matches as direct
34+
* barriers, so callers need not distinguish the two.
2835
*/
2936
class RegexMatch extends Expr instanceof RegexMatch::Range {
3037
/** Gets the expression for the regex being executed by this node. */
@@ -33,7 +40,13 @@ class RegexMatch extends Expr instanceof RegexMatch::Range {
3340
/** Gets an expression for the string to be searched or matched against. */
3441
Expr getString() { result = super.getString() }
3542

36-
/** Gets an expression to be sanitized. */
43+
/**
44+
* Gets an expression to be sanitized.
45+
*
46+
* To turn these into barrier nodes, use the `RegexMatchBarrier` module (in
47+
* `semmle.code.java.security.Sanitizers`), which correctly handles both
48+
* method-call and annotation matches.
49+
*/
3750
Expr getASanitizedExpr() { result = [this.getString(), super.getAdditionalSanitizedExpr()] }
3851

3952
/**

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ private class LineBreaksLogInjectionSanitizer extends LogInjectionSanitizer {
4040
LineBreaksLogInjectionSanitizer() {
4141
logInjectionSanitizer(this.asExpr())
4242
or
43-
this = DataFlow::BarrierGuard<logInjectionGuard/3>::getABarrierNode()
43+
this = RegexMatchBarrier<logInjectionGuard/3>::getABarrierNode()
4444
}
4545
}
4646

@@ -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 & 14 deletions
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 { }
@@ -470,12 +471,7 @@ private class DirectoryCharactersGuard extends PathGuard {
470471
Expr checkedExpr;
471472
boolean branch;
472473

473-
DirectoryCharactersGuard() {
474-
// Annotations are handled directly as barriers in `DirectoryCharactersSanitizer`,
475-
// since they don't dominate the sanitized expression and so can't act as barrier guards.
476-
not this instanceof Annotation and
477-
isMatchesCall(this, checkedExpr, branch)
478-
}
474+
DirectoryCharactersGuard() { isMatchesCall(this, checkedExpr, branch) }
479475

480476
override Expr getCheckedExpr() { result = checkedExpr }
481477

@@ -500,13 +496,6 @@ private class DirectoryCharactersSanitizer extends PathInjectionSanitizer {
500496
DirectoryCharactersSanitizer() {
501497
this.asExpr() instanceof ReplaceDirectoryCharactersSanitizer
502498
or
503-
this = DataFlow::BarrierGuard<directoryCharactersGuard/3>::getABarrierNode()
504-
or
505-
// Annotations don't fit into the model of barrier guards because the
506-
// annotation doesn't dominate the sanitized expression, so we instead
507-
// treat them as barriers directly.
508-
exists(RegexMatch rm | rm instanceof Annotation and isMatchesCall(rm, _, true) |
509-
this.asExpr() = rm.getString()
510-
)
499+
this = RegexMatchBarrier<directoryCharactersGuard/3>::getABarrierNode()
511500
}
512501
}

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

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,57 @@ 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+
* Used directly with `DataFlow::BarrierGuard`, only method-call matches yield
42+
* barrier nodes, since an annotation does not dominate the expression it
43+
* constrains. Use `RegexMatchBarrier` (as `RegexpCheckBarrier` does) to also
44+
* treat annotation matches as barriers.
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
4851
)
4952
}
5053

54+
/** A guard-check predicate over regular-expression matches, as used by `RegexMatchBarrier`. */
55+
signature predicate regexMatchGuardChecksSig(Guard g, Expr e, boolean branch);
56+
57+
/**
58+
* Given a guard-check predicate that includes regular-expression matches,
59+
* this module provides the corresponding barrier nodes, transparently handling
60+
* the two kinds of `RegexMatch`:
61+
* - method calls, which act as ordinary control-flow barrier guards, and
62+
* - annotations, which don't dominate the expressions they constrain and so
63+
* are instead treated as direct barriers.
64+
*
65+
* Callers do not need to distinguish between the two: include annotation
66+
* matches in `guardChecks` (with `branch = true`, since an annotation is
67+
* always present) and use `getABarrierNode` in place of a direct
68+
* `DataFlow::BarrierGuard` instantiation.
69+
*/
70+
module RegexMatchBarrier<regexMatchGuardChecksSig/3 guardChecks> {
71+
/** Gets a node that is sanitized by a regular-expression match. */
72+
DataFlow::Node getABarrierNode() {
73+
result = DataFlow::BarrierGuard<guardChecks/3>::getABarrierNode()
74+
or
75+
// An annotation doesn't dominate the expression it constrains, so the
76+
// barrier-guard machinery above never yields a node for it; treat it as a
77+
// direct barrier instead.
78+
exists(Guard g, Expr e |
79+
g instanceof Annotation and guardChecks(g, e, true) and result.asExpr() = e
80+
)
81+
}
82+
}
83+
5184
/**
5285
* A check against a regular expression, considered as a barrier guard.
5386
*
5487
* This is overapproximate: we do not attempt to reason about the correctness of the regexp.
5588
*/
5689
class RegexpCheckBarrier extends DataFlow::Node {
57-
RegexpCheckBarrier() {
58-
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())
64-
}
90+
RegexpCheckBarrier() { this = RegexMatchBarrier<regexpMatchGuardChecks/3>::getABarrierNode() }
6591
}
6692

6793
/**

0 commit comments

Comments
 (0)