Skip to content

Commit aee0b13

Browse files
authored
Merge pull request #22826 from owen-mc/java/constant-comparison-sanitizer
Java: Recognize constant string comparisons as taint sanitizers
2 parents 869263c + a620278 commit aee0b13

14 files changed

Lines changed: 544 additions & 324 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+
* Java taint-tracking queries now recognize equality and case-insensitive equality checks against fixed string constants as sanitizers. The queries `java/ssrf` and `java/unvalidated-url-redirection` also recognize checks of `URI.getHost()` against fixed collections of strings, including when the host is normalized to upper or lower case.

‎java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private module Frameworks {
3030
private import semmle.code.java.frameworks.ThreadLocal
3131
private import semmle.code.java.frameworks.ratpack.RatpackExec
3232
private import semmle.code.java.frameworks.stapler.Stapler
33-
private import semmle.code.java.security.ListOfConstantsSanitizer
33+
private import semmle.code.java.security.ConstantValueSanitizer
3434
private import semmle.code.java.security.PathSanitizer
3535
}
3636

java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll renamed to java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
/**
2-
* Provides a default taint sanitizer identifying comparisons against lists of
3-
* compile-time constants.
4-
*/
1+
/** Provides a default taint sanitizer identifying checks against compile-time constants. */
52
overlay[local?]
63
module;
74

@@ -10,6 +7,7 @@ private import codeql.typeflow.UniversalFlow as UniversalFlow
107
private import semmle.code.java.Collections
118
private import semmle.code.java.controlflow.Guards
129
private import semmle.code.java.dataflow.internal.BaseSSA as Base
10+
private import semmle.code.java.dataflow.NullGuards
1311
private import semmle.code.java.dataflow.TaintTracking
1412
private import semmle.code.java.dataflow.TypeFlow
1513
private import semmle.code.java.dispatch.VirtualDispatch
@@ -157,6 +155,29 @@ private predicate collectionWithPossibleMutation(FlowNode n) {
157155
)
158156
}
159157

158+
/** Holds if `n` is a collection constructed by an API that rejects null elements. */
159+
private predicate nullRejectingCollectionCreation(FlowNode n) {
160+
exists(Call c, Callable tgt |
161+
n.asExpr() = c and
162+
c.getCallee().getSourceDeclaration() = tgt
163+
|
164+
tgt.hasQualifiedName("java.util", ["List", "Set"], ["copyOf", "of"])
165+
or
166+
tgt.hasQualifiedName("com.google.common.collect", ["ImmutableList", "ImmutableSet"],
167+
["copyOf", "of"])
168+
)
169+
}
170+
171+
private module NullRejectingCollectionProp implements BaseUniversalFlow::NullaryPropertySig {
172+
predicate hasPropertyBase(FlowNode n) { nullRejectingCollectionCreation(n) }
173+
174+
predicate barrier = collectionWithPossibleMutation/1;
175+
}
176+
177+
/** Holds if the given collection cannot contain null. */
178+
private predicate nullRejectingCollection =
179+
BaseUniversalFlow::FlowNullary<NullRejectingCollectionProp>::hasProperty/1;
180+
160181
/**
161182
* A collection constructor that constructs an empty mutable collection.
162183
*/
@@ -241,27 +262,89 @@ private MethodCall normalizeCaseCall(Expr arg) {
241262
* Holds if the guard `g` ensures that the expression `e` is one of a set of
242263
* known constants upon evaluating to `branch`.
243264
*/
244-
private predicate constantCollectionContainsCheck(Guard g, Expr e, boolean branch) {
245-
exists(MethodCall mc, Method m, FlowNode n, Expr checked |
246-
g = mc and
247-
mc.getMethod().getSourceDeclaration().overridesOrInstantiates*(m) and
265+
private predicate constantCollectionContainsCall(
266+
MethodCall contains, FlowNode collection, Expr checked
267+
) {
268+
exists(Method m, FlowNode n |
269+
contains.getMethod().getSourceDeclaration().overridesOrInstantiates*(m) and
248270
m.hasQualifiedName("java.util", "Collection", "contains") and
249-
n.asExpr() = mc.getQualifier() and
271+
n.asExpr() = contains.getQualifier() and
250272
constantCollection(n) and
251-
checked = mc.getAnArgument() and
273+
checked = contains.getAnArgument() and
274+
collection = n
275+
)
276+
}
277+
278+
private predicate constantCollectionContainsCheck(Guard g, Expr e, boolean branch) {
279+
exists(Expr checked | checked = e or checked = normalizeCaseCall(e) |
280+
constantCollectionContainsCall(g, _, checked) and
281+
branch = true
282+
)
283+
}
284+
285+
/**
286+
* Holds if the guard `g` ensures that the string expression `e` equals a compile-time constant
287+
* upon evaluating to `branch`.
288+
*/
289+
private predicate constantStringEqualityCheck(Guard g, Expr e, boolean branch) {
290+
exists(MethodCall comparison, Expr constant |
291+
g = comparison and
292+
comparison.getMethod().getDeclaringType() instanceof TypeString and
293+
comparison.getMethod().hasName(["equals", "equalsIgnoreCase"]) and
294+
branch = true and
295+
(
296+
e = comparison.getQualifier() and constant = comparison.getArgument(0)
297+
or
298+
e = comparison.getArgument(0) and constant = comparison.getQualifier()
299+
) and
300+
exists(constant.(CompileTimeConstantExpr).getStringValue())
301+
)
302+
}
303+
304+
/**
305+
* Holds if the guard `g` ensures that the expression `e` is one of a set of compile-time constants
306+
* upon evaluating to `branch`.
307+
*/
308+
predicate constantValueGuardChecks(Guard g, Expr e, boolean branch) {
309+
constantStringEqualityCheck(g, e, branch)
310+
or
311+
constantCollectionContainsCheck(g, e, branch)
312+
}
313+
314+
/**
315+
* Holds if `g` ensures that `e` is a non-null member of a set of compile-time constants upon
316+
* evaluating to `branch`.
317+
*/
318+
predicate nonNullConstantValueGuardChecks(Guard g, Expr e, boolean branch) {
319+
constantStringEqualityCheck(g, e, branch)
320+
or
321+
exists(FlowNode collection, Expr checked |
322+
constantCollectionContainsCall(g, collection, checked) and
252323
branch = true
253324
|
254-
checked = e or
255-
checked = normalizeCaseCall(e)
325+
checked = e and
326+
(
327+
nullRejectingCollection(collection)
328+
or
329+
e = clearlyNotNullExpr()
330+
)
331+
or
332+
checked = normalizeCaseCall(e) and
333+
(
334+
nullRejectingCollection(collection)
335+
or
336+
checked.(MethodCall).getQualifier() = e
337+
or
338+
e = clearlyNotNullExpr()
339+
)
256340
)
257341
}
258342

259343
/**
260-
* A comparison against a list of compile-time constants, sanitizing taint by
261-
* restricting to a set of known values.
344+
* A check against compile-time constants, sanitizing taint by restricting a value to a known set.
262345
*/
263-
private class ListOfConstantsComparisonSanitizerGuard extends TaintTracking::DefaultTaintSanitizer {
264-
ListOfConstantsComparisonSanitizerGuard() {
265-
this = DataFlow::BarrierGuard<constantCollectionContainsCheck/3>::getABarrierNode()
346+
private class ConstantValueSanitizerGuard extends TaintTracking::DefaultTaintSanitizer {
347+
ConstantValueSanitizerGuard() {
348+
this = DataFlow::BarrierGuard<constantValueGuardChecks/3>::getABarrierNode()
266349
}
267350
}

‎java/ql/lib/semmle/code/java/security/RequestForgery.qll‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ private import semmle.code.java.controlflow.Guards
1414
private import semmle.code.java.dataflow.StringPrefixes
1515
private import semmle.code.java.dataflow.ExternalFlow
1616
private import semmle.code.java.dataflow.SSA
17+
private import semmle.code.java.security.ConstantValueSanitizer
1718
private import semmle.code.java.security.Sanitizers
1819

1920
/**
@@ -149,18 +150,8 @@ private predicate isHostComparisonSanitizer(Guard guard, Expr e, boolean branch)
149150
)
150151
)
151152
or
152-
exists(MethodCall comparison, Expr checked, Expr allowed |
153-
guard = comparison and
154-
comparison.getMethod().getDeclaringType() instanceof TypeString and
155-
comparison.getMethod().hasName(["equals", "equalsIgnoreCase"]) and
156-
branch = true and
157-
(
158-
checked = comparison.getQualifier() and allowed = comparison.getArgument(0)
159-
or
160-
checked = comparison.getArgument(0) and allowed = comparison.getQualifier()
161-
) and
162-
// New cases require a fixed allowlist entry, not another untrusted value.
163-
exists(allowed.(CompileTimeConstantExpr).getStringValue()) and
153+
exists(Expr checked |
154+
nonNullConstantValueGuardChecks(guard, checked, branch) and
164155
e = getCheckedHost(checked).getQualifier()
165156
)
166157
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
| Type ConstantValue uses out-of-scope type variable E. Note the Java extractor is known to sometimes do this; the Kotlin extractor should not. |

java/ql/test/library-tests/listofconstants/A.java renamed to java/ql/test/library-tests/default-taint-sanitizer/ConstantValue.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import java.util.*;
22

3-
public class A {
3+
public class ConstantValue {
44
private static final Set<String> SEPARATORS =
55
Collections.unmodifiableSet(
66
new HashSet<>(Arrays.asList("\t", "\n", ";")));
@@ -32,4 +32,28 @@ private void testAdd(String inp) {
3232
sink(inp);
3333
}
3434
}
35+
36+
private void checkEquals(String input) {
37+
if ("AA".equals(input)) {
38+
sink(input);
39+
}
40+
}
41+
42+
private void checkEqualsIgnoreCase(String input) {
43+
if (input.equalsIgnoreCase("AA")) {
44+
sink(input);
45+
}
46+
}
47+
48+
private void checkWrongBranch(String input) {
49+
if (!"AA".equals(input)) {
50+
sink(input);
51+
}
52+
}
53+
54+
private void checkUntrustedValue(String input, String allowed) {
55+
if (allowed.equals(input)) {
56+
sink(input);
57+
}
58+
}
3559
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| ConstantValue.java:12:12:12:20 | separator |
2+
| ConstantValue.java:23:12:23:17 | schema |
3+
| ConstantValue.java:32:12:32:14 | inp |
4+
| ConstantValue.java:38:12:38:16 | input |
5+
| ConstantValue.java:44:12:44:16 | input |

java/ql/test/library-tests/listofconstants/test.ql renamed to java/ql/test/library-tests/default-taint-sanitizer/test.ql

File renamed without changes.

‎java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎java/ql/test/library-tests/listofconstants/test.expected‎

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)