-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Java: Add a default taint sanitizer for contains-checks on lists of constants #17901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
aschackmull
merged 11 commits into
github:main
from
aschackmull:java/allowlist-sanitizer
Nov 27, 2024
Merged
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
7f86f8c
Java: Prepare TypeFlow for separate instantiation of universal flow.
aschackmull 6f32c41
Java: Add a default taint sanitizer for contains-checks on lists of c…
aschackmull 5a4b720
Java: Add change note.
aschackmull 2b1caa8
Java: Add test.
aschackmull 0d45f0e
Java: Accept consistency check result.
aschackmull 408a38d
Java: Address review comment, include addFirst,addLast.
aschackmull 2ff2d25
Java: Cherry-pick test from https://github.com/github/codeql/pull/17051
aschackmull 38eb3e4
Java: Adjust expected output.
aschackmull a6fc41e
Java: Accept consistency failure.
aschackmull 85778f7
Java: Fix semantic merge conflict in expected file.
aschackmull 5ef496d
Java: Add more qldoc.
aschackmull File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
java/ql/lib/change-notes/2024-11-04-list-of-constants-sanitizer.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| --- | ||
| category: minorAnalysis | ||
| --- | ||
| * Calling `coll.contains(x)` is now a taint sanitizer (for any query) for the value `x`, where `coll` is a collection of constants. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
263 changes: 263 additions & 0 deletions
263
java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| /** | ||
| * Provides a default taint sanitizer identifying comparisons against lists of | ||
| * compile-time constants. | ||
| */ | ||
|
|
||
| import java | ||
| private import codeql.typeflow.UniversalFlow as UniversalFlow | ||
| private import semmle.code.java.Collections | ||
| private import semmle.code.java.controlflow.Guards | ||
| private import semmle.code.java.dataflow.internal.BaseSSA | ||
| private import semmle.code.java.dataflow.TaintTracking | ||
| private import semmle.code.java.dataflow.TypeFlow | ||
| private import semmle.code.java.dispatch.VirtualDispatch | ||
|
|
||
| private class FlowNode = FlowStepsInput::FlowNode; | ||
|
|
||
| /** | ||
| * Holds if `n2` is an unmodifiable collection constructed from input `n1`, | ||
| * which is either another collection or a number of elements. | ||
| */ | ||
| private predicate unmodifiableCollectionStep(FlowNode n1, FlowNode n2) { | ||
| exists(Call c, Callable tgt | | ||
| n2.asExpr() = c and | ||
| n1.asExpr() = c.getAnArgument() and | ||
| c.getCallee().getSourceDeclaration() = tgt | ||
| | | ||
| tgt.hasQualifiedName("java.util", "Collections", | ||
| ["unmodifiableCollection", "unmodifiableList", "unmodifiableSet"]) | ||
| or | ||
| tgt.hasQualifiedName("java.util", ["List", "Set"], ["copyOf", "of"]) | ||
| or | ||
| tgt.hasQualifiedName("com.google.common.collect", ["ImmutableList", "ImmutableSet"], | ||
| ["copyOf", "of"]) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Holds if `n2` is a collection or array constructed from input `n1`, which is | ||
| * either a collection, an array, or a number of elements. | ||
| */ | ||
| private predicate collectionStep(FlowNode n1, FlowNode n2) { | ||
| n2.asExpr().(ArrayInit).getAnInit() = n1.asExpr() | ||
| or | ||
| n2.asExpr().(ArrayCreationExpr).getInit() = n1.asExpr() | ||
| or | ||
| unmodifiableCollectionStep(n1, n2) | ||
| or | ||
| exists(Call c, Callable tgt | | ||
| n2.asExpr() = c and | ||
| n1.asExpr() = c.getAnArgument() and | ||
| c.getCallee().getSourceDeclaration() = tgt | ||
| | | ||
| tgt.hasQualifiedName("java.util", "Arrays", "asList") | ||
| or | ||
| tgt.isStatic() and | ||
| tgt.hasName(["copyOf", "of"]) and | ||
| tgt.getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util", "Collection") | ||
| or | ||
| tgt instanceof Constructor and | ||
| tgt.getNumberOfParameters() = 1 and | ||
| tgt.getParameterType(0) instanceof CollectionType and | ||
| tgt.getDeclaringType() instanceof CollectionType | ||
| ) | ||
| } | ||
|
|
||
| private module BaseUniversalFlow = UniversalFlow::Make<Location, FlowStepsInput>; | ||
|
|
||
| private module UnmodifiableProp implements BaseUniversalFlow::NullaryPropertySig { | ||
| predicate hasPropertyBase(FlowNode n) { unmodifiableCollectionStep(_, n) } | ||
| } | ||
|
|
||
| /** Holds if the given node is an unmodifiable collection. */ | ||
| private predicate unmodifiableCollection = | ||
| BaseUniversalFlow::FlowNullary<UnmodifiableProp>::hasProperty/1; | ||
|
|
||
| /** | ||
| * Holds if `v` is a collection or array with an access, `coll`, at which the | ||
| * element `e` gets added. | ||
| */ | ||
| private predicate collectionAddition(Variable v, VarAccess coll, Expr e) { | ||
| exists(MethodCall mc, Method m, int arg | | ||
| mc.getMethod().getSourceDeclaration().overridesOrInstantiates*(m) and | ||
| mc.getQualifier() = coll and | ||
| v.getAnAccess() = coll and | ||
| mc.getArgument(arg) = e | ||
| | | ||
| m.hasQualifiedName("java.util", "Collection", ["add", "addAll"]) and | ||
| m.getNumberOfParameters() = 1 and | ||
| arg = 0 | ||
| or | ||
| m.hasQualifiedName("java.util", "List", ["add", "addAll"]) and | ||
| m.getNumberOfParameters() = 2 and | ||
| arg = 1 | ||
|
Comment on lines
+87
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't expect allowlists constructions to use those, but I've added them now - it can't hurt. |
||
| or | ||
| m.hasQualifiedName("java.util", "SequencedCollection", ["addFirst", "addLast"]) and | ||
| m.getNumberOfParameters() = 1 and | ||
| arg = 0 | ||
| ) | ||
| or | ||
| v.getAnAccess() = coll and | ||
| exists(Assignment assign | assign.getSource() = e | | ||
| coll = assign.getDest().(ArrayAccess).getArray() | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Holds if `n` represents a definition of `v` and `v` is a collection or | ||
| * array that has additions occurring as side-effects after its definition. | ||
| */ | ||
| private predicate nodeWithAddition(FlowNode n, Variable v) { | ||
| collectionAddition(v, _, _) and | ||
| ( | ||
| n.asField() = v | ||
| or | ||
| n.asSsa().getSourceVariable().getVariable() = v and | ||
| (n.asSsa() instanceof BaseSsaUpdate or n.asSsa().(BaseSsaImplicitInit).isParameterDefinition(_)) | ||
| ) | ||
| } | ||
|
|
||
| /** Holds if `c` does not add elements to the given collection. */ | ||
| private predicate safeCallable(Callable c) { | ||
| c instanceof CollectionQueryMethod | ||
| or | ||
| c instanceof CollectionMethod and | ||
| c.hasName(["clear", "remove", "removeAll", "stream", "iterator", "toArray"]) | ||
| or | ||
| c.hasQualifiedName("org.apache.commons.lang3", "StringUtils", "join") | ||
| } | ||
|
|
||
| /** | ||
| * Holds if `n` might be mutated in ways that adds elements that are not | ||
| * tracked by the `collectionAddition` predicate. | ||
| */ | ||
| private predicate collectionWithPossibleMutation(FlowNode n) { | ||
| not unmodifiableCollection(n) and | ||
| ( | ||
| exists(Expr e | | ||
| n.asExpr() = e and | ||
| (e.getType() instanceof CollectionType or e.getType() instanceof Array) and | ||
| not collectionAddition(_, e, _) and | ||
| not collectionStep(n, _) | ||
| | | ||
| exists(ArrayAccess aa | e = aa.getArray()) | ||
| or | ||
| exists(Call c, Callable tgt | c.getAnArgument() = e or c.getQualifier() = e | | ||
| tgt = c.getCallee().getSourceDeclaration() and | ||
| not safeCallable(tgt) | ||
| ) | ||
| ) | ||
| or | ||
| exists(FlowNode mid | | ||
| FlowStepsInput::step(n, mid) and | ||
| collectionWithPossibleMutation(mid) | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * A collection constructor that constructs an empty mutable collection. | ||
| */ | ||
| private class EmptyCollectionConstructor extends Constructor { | ||
| EmptyCollectionConstructor() { | ||
| this.getDeclaringType() instanceof CollectionType and | ||
| forall(Type t | t = this.getAParamType() | t instanceof PrimitiveType) | ||
| } | ||
| } | ||
|
|
||
| private module CollectionFlowStepsInput implements UniversalFlow::UniversalFlowInput<Location> { | ||
| import FlowStepsInput | ||
|
|
||
| /** | ||
| * Holds if `n2` is a collection/array/constant whose value(s) are | ||
| * determined completely from the range of `n1` nodes. | ||
| */ | ||
| predicate step(FlowNode n1, FlowNode n2) { | ||
| // Exclude the regular input constraints for those nodes that are covered | ||
| // completely by `collectionStep`. | ||
| FlowStepsInput::step(n1, n2) and | ||
| not collectionStep(_, n2) | ||
| or | ||
| // For collections with side-effects in the form of additions, we add the | ||
| // sources of those additions as additional input that need to originate | ||
| // from constants. | ||
| exists(Variable v | | ||
| nodeWithAddition(n2, v) and | ||
| collectionAddition(v, _, n1.asExpr()) | ||
| ) | ||
| or | ||
| // Include various forms of collection transformation. | ||
| collectionStep(n1, n2) | ||
| } | ||
|
|
||
| predicate isExcludedFromNullAnalysis = FlowStepsInput::isExcludedFromNullAnalysis/1; | ||
| } | ||
|
|
||
| private module CollectionUniversalFlow = UniversalFlow::Make<Location, CollectionFlowStepsInput>; | ||
|
|
||
| private module ConstantCollectionProp implements CollectionUniversalFlow::NullaryPropertySig { | ||
| /** | ||
| * Holds if `n` forms the base case for finding collections of constants. | ||
| * These are individual constants and empty collections. | ||
| */ | ||
| predicate hasPropertyBase(FlowNode n) { | ||
| n.asExpr().isCompileTimeConstant() or | ||
| n.asExpr().(ConstructorCall).getConstructor() instanceof EmptyCollectionConstructor | ||
| } | ||
|
|
||
| predicate barrier = collectionWithPossibleMutation/1; | ||
| } | ||
|
|
||
| /** | ||
| * Holds if the given node is either a constant or a collection/array of | ||
| * constants. | ||
| */ | ||
| private predicate constantCollection = | ||
| CollectionUniversalFlow::FlowNullary<ConstantCollectionProp>::hasProperty/1; | ||
|
|
||
| /** Gets the result of a case normalization call of `arg`. */ | ||
| private MethodCall normalizeCaseCall(Expr arg) { | ||
| exists(Method changecase | result.getMethod() = changecase | | ||
| changecase.hasName(["toUpperCase", "toLowerCase"]) and | ||
| changecase.getDeclaringType() instanceof TypeString and | ||
| arg = result.getQualifier() | ||
| or | ||
| changecase | ||
| .hasQualifiedName(["org.apache.commons.lang", "org.apache.commons.lang3"], "StringUtils", | ||
| ["lowerCase", "upperCase"]) and | ||
| arg = result.getArgument(0) | ||
| or | ||
| changecase | ||
| .hasQualifiedName("org.apache.hadoop.util", "StringUtils", ["toLowerCase", "toUpperCase"]) and | ||
| arg = result.getArgument(0) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Holds if the guard `g` ensures that the expression `e` is one of a set of | ||
| * known constants upon evaluating to `branch`. | ||
| */ | ||
| private predicate constantCollectionContainsCheck(Guard g, Expr e, boolean branch) { | ||
| exists(MethodCall mc, Method m, FlowNode n, Expr checked | | ||
| g = mc and | ||
| mc.getMethod().getSourceDeclaration().overridesOrInstantiates*(m) and | ||
| m.hasQualifiedName("java.util", "Collection", "contains") and | ||
| n.asExpr() = mc.getQualifier() and | ||
| constantCollection(n) and | ||
| checked = mc.getAnArgument() and | ||
| branch = true | ||
| | | ||
| checked = e or | ||
| checked = normalizeCaseCall(e) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * A comparison against a list of compile-time constants, sanitizing taint by | ||
| * restricting to a set of known values. | ||
| */ | ||
| private class ListOfConstantsComparisonSanitizerGuard extends TaintTracking::DefaultTaintSanitizer { | ||
| ListOfConstantsComparisonSanitizerGuard() { | ||
| this = DataFlow::BarrierGuard<constantCollectionContainsCheck/3>::getABarrierNode() | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / CodeQL
Redundant import Warning