Skip to content

Commit d744cf9

Browse files
committed
Clean up guard logic:
* Always sanitize after the second guard, not the first * Only check basic-block dominance in one place * One BarrierGuard extension per final guard
1 parent 748008a commit d744cf9

File tree

1 file changed

+68
-41
lines changed

1 file changed

+68
-41
lines changed

java/ql/src/experimental/Security/CWE/CWE-552/UnsafeUrlForward.qll

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -74,68 +74,99 @@ private class ExactStringPathMatchGuard extends UnsafeUrlForwardBarrierGuard ins
7474
}
7575
}
7676

77+
private class AllowListGuard extends Guard instanceof MethodAccess {
78+
AllowListGuard() {
79+
(isStringPartialMatch(this.(MethodAccess)) or isPathPartialMatch(this.(MethodAccess))) and
80+
not isDisallowedWord(this.(MethodAccess).getAnArgument())
81+
}
82+
83+
Expr getCheckedExpr() { result = super.getQualifier() }
84+
}
85+
7786
/**
78-
* A guard that considers safe a string being matched against an allowlist of partial trusted values.
87+
* A guard that considers a path safe because it is checked against an allowlist of partial trusted values.
7988
* This requires additional protection against path traversal, either another guard (`PathTraversalGuard`)
80-
* or a sanitizer (`PathNormalizeSanitizer`).
89+
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
8190
*/
82-
private class AllowListCheckGuard extends UnsafeUrlForwardBarrierGuard instanceof MethodAccess {
83-
AllowListCheckGuard() {
84-
(isStringPartialMatch(this) or isPathPartialMatch(this)) and
85-
not isDisallowedWord(this.getAnArgument())
86-
}
87-
91+
private class AllowListBarrierGuard extends UnsafeUrlForwardBarrierGuard instanceof AllowListGuard {
8892
override predicate checks(Expr e, boolean branch) {
89-
e = this.(MethodAccess).getQualifier() and
93+
e = super.getCheckedExpr() and
9094
branch = true and
9195
(
92-
// Either the path normalization sanitizer comes before the guard
96+
// Either a path normalization sanitizer comes before the guard,
9397
exists(PathNormalizeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e))
9498
or
95-
// or the path traversal check comes before the guard
96-
exists(PathTraversalGuard guard |
97-
guard.checks(any(Expr checked | DataFlow::localExprFlow(checked, e))) or
98-
// or both checks are in the same condition
99-
// (for example, `path.startsWith(BASE_PATH) && !path.contains("..")`)
100-
guard.controls(this.getBasicBlock().(ConditionBlock), false) or
101-
this.controls(guard.getBasicBlock().(ConditionBlock), branch)
99+
// or a check like `!path.contains("..")` comes before the guard
100+
exists(PathTraversalGuard previousGuard |
101+
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
102+
previousGuard.controls(this.getBasicBlock().(ConditionBlock), false)
102103
)
103104
)
104105
}
105106
}
106107

107108
/**
108-
* A guard that considers safe a string being matched against a blocklist of known dangerous values.
109-
* This requires additional protection against path traversal, either another guard (`UrlEncodingGuard`)
110-
* or a sanitizer (`UrlDecodeSanitizer`).
109+
* A guard that considers a path safe because it is checked for `..` components, having previously
110+
* been checked for a trusted prefix.
111111
*/
112-
private class BlockListCheckGuard extends UnsafeUrlForwardBarrierGuard instanceof MethodAccess {
113-
BlockListCheckGuard() {
112+
private class DotDotCheckBarrierGuard extends UnsafeUrlForwardBarrierGuard instanceof PathTraversalGuard {
113+
override predicate checks(Expr e, boolean branch) {
114+
e = super.getCheckedExpr() and
115+
branch = false and
116+
// The same value has previously been checked against a list of allowed prefixes:
117+
exists(AllowListGuard previousGuard |
118+
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
119+
previousGuard.controls(this.getBasicBlock().(ConditionBlock), true)
120+
)
121+
}
122+
}
123+
124+
private class BlockListGuard extends Guard instanceof MethodAccess {
125+
BlockListGuard() {
114126
(isStringPartialMatch(this) or isPathPartialMatch(this)) and
115127
isDisallowedWord(this.getAnArgument())
116128
}
117129

130+
Expr getCheckedExpr() { result = super.getQualifier() }
131+
}
132+
133+
/**
134+
* A guard that considers a string safe because it is checked against a blocklist of known dangerous values.
135+
* This requires a prior check for URL encoding concealing a forbidden value, either a guard (`UrlEncodingGuard`)
136+
* or a sanitizer (`UrlDecodeSanitizer`).
137+
*/
138+
private class BlockListBarrierGuard extends UnsafeUrlForwardBarrierGuard instanceof BlockListGuard {
118139
override predicate checks(Expr e, boolean branch) {
119-
e = this.(MethodAccess).getQualifier() and
140+
e = super.getCheckedExpr() and
120141
branch = false and
121142
(
122-
// Either the URL decode sanitization comes before the guard
143+
// Either `e` has been URL decoded:
123144
exists(UrlDecodeSanitizer sanitizer | DataFlow::localExprFlow(sanitizer, e))
124145
or
125-
// or the URL encoding check comes before the guard
126-
exists(UrlEncodingGuard guard |
127-
guard.checks(any(Expr checked | DataFlow::localExprFlow(checked, e)))
128-
or
129-
// or both checks are in the same condition
130-
// (for example, `!path.contains("..") && !path.contains("%")`)
131-
guard.controls(this.getBasicBlock().(ConditionBlock), false)
132-
or
133-
this.controls(guard.getBasicBlock().(ConditionBlock), branch)
146+
// or `e` has previously been checked for URL encoding sequences:
147+
exists(UrlEncodingGuard previousGuard |
148+
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
149+
previousGuard.controls(this.getBasicBlock(), false)
134150
)
135151
)
136152
}
137153
}
138154

155+
/**
156+
* A guard that considers a string safe because it is checked for URL encoding sequences,
157+
* having previously been checked against a block-list of forbidden values.
158+
*/
159+
private class URLEncodingBarrierGuard extends UnsafeUrlForwardBarrierGuard instanceof UrlEncodingGuard {
160+
override predicate checks(Expr e, boolean branch) {
161+
e = super.getCheckedExpr() and
162+
branch = false and
163+
exists(BlockListGuard previousGuard |
164+
DataFlow::localExprFlow(previousGuard.getCheckedExpr(), e) and
165+
previousGuard.controls(this.getBasicBlock(), false)
166+
)
167+
}
168+
}
169+
139170
/**
140171
* Holds if `ma` is a call to a method that checks a partial string match.
141172
*/
@@ -164,11 +195,10 @@ private class PathTraversalGuard extends Guard instanceof MethodAccess {
164195
PathTraversalGuard() {
165196
this.getMethod().getDeclaringType() instanceof TypeString and
166197
this.getMethod().hasName(["contains", "indexOf"]) and
167-
this.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".." and
168-
this.controls(checked.getBasicBlock(), false)
198+
this.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
169199
}
170200

171-
predicate checks(Expr e) { checked = e }
201+
Expr getCheckedExpr() { result = super.getQualifier() }
172202
}
173203

174204
/** A complementary sanitizer that protects against path traversal using path normalization. */
@@ -181,16 +211,13 @@ private class PathNormalizeSanitizer extends MethodAccess {
181211

182212
/** A complementary guard that protects against double URL encoding, by looking for the literal `%`. */
183213
private class UrlEncodingGuard extends Guard instanceof MethodAccess {
184-
Expr checked;
185-
186214
UrlEncodingGuard() {
187215
this.getMethod().getDeclaringType() instanceof TypeString and
188216
this.getMethod().hasName(["contains", "indexOf"]) and
189-
this.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%" and
190-
this.controls(checked.getBasicBlock(), false)
217+
this.getAnArgument().(CompileTimeConstantExpr).getStringValue() = "%"
191218
}
192219

193-
predicate checks(Expr e) { checked = e }
220+
Expr getCheckedExpr() { result = super.getQualifier() }
194221
}
195222

196223
/** A complementary sanitizer that protects against double URL encoding using URL decoding. */

0 commit comments

Comments
 (0)