Skip to content

Commit 891694b

Browse files
authored
Merge pull request #5908 from erik-krogh/protoLib
JS: Add library input as source to js/prototype-polluting-assignment
2 parents 140a70f + 56a7c8b commit 891694b

File tree

13 files changed

+520
-63
lines changed

13 files changed

+520
-63
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* The `js/prototype-polluting-assignment` query now flags assignments that may modify
3+
the built-in Object prototype where the property name originates from library input.

javascript/ql/lib/semmle/javascript/PackageExports.qll

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,36 @@ private import semmle.javascript.internal.CachedStages
1111
* Gets a parameter that is a library input to a top-level package.
1212
*/
1313
cached
14-
DataFlow::ParameterNode getALibraryInputParameter() {
14+
DataFlow::SourceNode getALibraryInputParameter() {
1515
Stages::Taint::ref() and
1616
exists(int bound, DataFlow::FunctionNode func |
17-
func = getAValueExportedByPackage().getABoundFunctionValue(bound) and
17+
func = getAValueExportedByPackage().getABoundFunctionValue(bound)
18+
|
1819
result = func.getParameter(any(int arg | arg >= bound))
20+
or
21+
result = getAnArgumentsRead(func.getFunction())
22+
)
23+
}
24+
25+
private DataFlow::SourceNode getAnArgumentsRead(Function func) {
26+
exists(DataFlow::PropRead read |
27+
not read.getPropertyName() = "length" and
28+
result = read
29+
|
30+
read.getBase() = func.getArgumentsVariable().getAnAccess().flow()
31+
or
32+
exists(DataFlow::MethodCallNode call |
33+
call =
34+
DataFlow::globalVarRef("Array")
35+
.getAPropertyRead("prototype")
36+
.getAPropertyRead("slice")
37+
.getAMethodCall("call")
38+
or
39+
call = DataFlow::globalVarRef("Array").getAMethodCall("from")
40+
|
41+
call.getArgument(0) = func.getArgumentsVariable().getAnAccess().flow() and
42+
call.flowsTo(read.getBase())
43+
)
1944
)
2045
}
2146

javascript/ql/lib/semmle/javascript/filters/ClassifyFiles.qll

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,49 @@ private predicate looksLikeExterns(TopLevel tl) {
4545
)
4646
}
4747

48+
/**
49+
* Holds if `f` contains generated or minified code.
50+
*/
51+
predicate isGeneratedCodeFile(File f) { isGenerated(f.getATopLevel()) }
52+
53+
/**
54+
* Holds if `f` contains test code.
55+
*/
56+
predicate isTestFile(File f) {
57+
exists(Test t | t.getFile() = f)
58+
or
59+
exists(string stemExt | stemExt = "test" or stemExt = "spec" |
60+
f = getTestFile(any(File orig), stemExt)
61+
)
62+
or
63+
f.getAbsolutePath().regexpMatch(".*/__(mocks|tests)__/.*")
64+
}
65+
66+
/**
67+
* Holds if `f` contains externs declarations.
68+
*/
69+
predicate isExternsFile(File f) {
70+
(f.getATopLevel().isExterns() or looksLikeExterns(f.getATopLevel()))
71+
}
72+
73+
/**
74+
* Holds if `f` contains library code.
75+
*/
76+
predicate isLibaryFile(File f) { f.getATopLevel() instanceof FrameworkLibraryInstance }
77+
78+
/**
79+
* Holds if `f` contains template code.
80+
*/
81+
predicate isTemplateFile(File f) {
82+
exists(JSParseError err | maybeCausedByTemplate(err) | f = err.getFile())
83+
or
84+
// Polymer templates
85+
exists(HTML::Element elt | elt.getName() = "template" |
86+
f = elt.getFile() and
87+
not f.getExtension() = "vue"
88+
)
89+
}
90+
4891
/**
4992
* Holds if `f` is classified as belonging to `category`.
5093
*
@@ -55,33 +98,15 @@ private predicate looksLikeExterns(TopLevel tl) {
5598
* - `"library"`: `f` contains library code;
5699
* - `"template"`: `f` contains template code.
57100
*/
101+
pragma[inline]
58102
predicate classify(File f, string category) {
59-
isGenerated(f.getATopLevel()) and category = "generated"
60-
or
61-
(
62-
exists(Test t | t.getFile() = f)
63-
or
64-
exists(string stemExt | stemExt = "test" or stemExt = "spec" |
65-
f = getTestFile(any(File orig), stemExt)
66-
)
67-
or
68-
f.getAbsolutePath().regexpMatch(".*/__(mocks|tests)__/.*")
69-
) and
70-
category = "test"
103+
isGeneratedCodeFile(f) and category = "generated"
71104
or
72-
(f.getATopLevel().isExterns() or looksLikeExterns(f.getATopLevel())) and
73-
category = "externs"
105+
isTestFile(f) and category = "test"
74106
or
75-
f.getATopLevel() instanceof FrameworkLibraryInstance and category = "library"
107+
isExternsFile(f) and category = "externs"
76108
or
77-
exists(JSParseError err | maybeCausedByTemplate(err) |
78-
f = err.getFile() and category = "template"
79-
)
109+
isLibaryFile(f) and category = "library"
80110
or
81-
// Polymer templates
82-
exists(HTML::Element elt | elt.getName() = "template" |
83-
f = elt.getFile() and
84-
category = "template" and
85-
not f.getExtension() = "vue"
86-
)
111+
isTemplateFile(f) and category = "template"
87112
}

javascript/ql/lib/semmle/javascript/security/dataflow/PrototypePollutingAssignmentCustomizations.qll

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ module PrototypePollutingAssignment {
1313
/**
1414
* A data flow source for untrusted data from which the special `__proto__` property name may be arise.
1515
*/
16-
abstract class Source extends DataFlow::Node { }
16+
abstract class Source extends DataFlow::Node {
17+
/**
18+
* Gets a string that describes the type of source.
19+
*/
20+
abstract string describe();
21+
}
1722

1823
/**
1924
* A data flow sink for prototype-polluting assignments or untrusted property names.
@@ -44,6 +49,8 @@ module PrototypePollutingAssignment {
4449
this = any(DataFlow::PropWrite write).getBase()
4550
or
4651
this = any(ExtendCall c).getDestinationOperand()
52+
or
53+
this = any(DeleteExpr del).getOperand().flow().(DataFlow::PropRef).getBase()
4754
}
4855

4956
override DataFlow::FlowLabel getAFlowLabel() { result instanceof ObjectPrototype }
@@ -52,5 +59,18 @@ module PrototypePollutingAssignment {
5259
/** A remote flow source or location.{hash,search} as a taint source. */
5360
private class DefaultSource extends Source {
5461
DefaultSource() { this instanceof RemoteFlowSource }
62+
63+
override string describe() { result = "user controlled input" }
64+
}
65+
66+
import semmle.javascript.PackageExports as Exports
67+
68+
/**
69+
* A parameter of an exported function, seen as a source prototype-polluting assignment.
70+
*/
71+
class ExternalInputSource extends Source, DataFlow::SourceNode {
72+
ExternalInputSource() { this = Exports::getALibraryInputParameter() }
73+
74+
override string describe() { result = "library input" }
5575
}
5676
}

javascript/ql/lib/semmle/javascript/security/dataflow/PrototypePollutingAssignmentQuery.qll

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
private import javascript
1111
private import semmle.javascript.DynamicPropertyAccess
1212
private import semmle.javascript.dataflow.InferredTypes
13-
private import PrototypePollutingAssignmentCustomizations::PrototypePollutingAssignment
13+
import PrototypePollutingAssignmentCustomizations::PrototypePollutingAssignment
14+
private import semmle.javascript.filters.ClassifyFiles as ClassifyFiles
1415

1516
// Materialize flow labels
1617
private class ConcreteObjectPrototype extends ObjectPrototype {
@@ -31,7 +32,10 @@ class Configuration extends TaintTracking::Configuration {
3132
node instanceof Sanitizer
3233
or
3334
// Concatenating with a string will in practice prevent the string `__proto__` from arising.
34-
node instanceof StringOps::ConcatenationRoot
35+
exists(StringOps::ConcatenationRoot root | node = root |
36+
// Exclude the string coercion `"" + node` from this filter.
37+
not node.(StringOps::ConcatenationNode).isCoercion()
38+
)
3539
or
3640
node instanceof DataFlow::ThisNode
3741
or
@@ -79,6 +83,29 @@ class Configuration extends TaintTracking::Configuration {
7983
inlbl.isTaint() and
8084
outlbl instanceof ObjectPrototype
8185
)
86+
or
87+
DataFlow::localFieldStep(pred, succ) and inlbl = outlbl
88+
}
89+
90+
override predicate hasFlowPath(DataFlow::SourcePathNode source, DataFlow::SinkPathNode sink) {
91+
super.hasFlowPath(source, sink) and
92+
// require that there is a path without unmatched return steps
93+
DataFlow::hasPathWithoutUnmatchedReturn(source, sink) and
94+
// filter away paths that start with library inputs and end with a write to a fixed property.
95+
not exists(ExternalInputSource src, Sink snk, DataFlow::PropWrite write |
96+
source.getNode() = src and sink.getNode() = snk
97+
|
98+
snk = write.getBase() and
99+
(
100+
// fixed property name
101+
exists(write.getPropertyName())
102+
or
103+
// non-string property name (likely number)
104+
exists(Expr prop | prop = write.getPropertyNameExpr() |
105+
not prop.analyze().getAType() = TTString()
106+
)
107+
)
108+
)
82109
}
83110

84111
override predicate isLabeledBarrier(DataFlow::Node node, DataFlow::FlowLabel lbl) {
@@ -95,7 +122,8 @@ class Configuration extends TaintTracking::Configuration {
95122
guard instanceof InstanceofCheck or
96123
guard instanceof IsArrayCheck or
97124
guard instanceof TypeofCheck or
98-
guard instanceof EqualityCheck
125+
guard instanceof EqualityCheck or
126+
guard instanceof IncludesCheck
99127
}
100128
}
101129

@@ -108,7 +136,8 @@ private DataFlow::SourceNode prototypeLessObject(DataFlow::TypeTracker t) {
108136
t.start() and
109137
// We assume the argument to Object.create is not Object.prototype, since most
110138
// users wouldn't bother to call Object.create in that case.
111-
result = DataFlow::globalVarRef("Object").getAMemberCall("create")
139+
result = DataFlow::globalVarRef("Object").getAMemberCall("create") and
140+
not result.getFile() instanceof TestFile
112141
or
113142
// Allow use of SharedFlowSteps to track a bit further
114143
exists(DataFlow::Node mid |
@@ -119,6 +148,14 @@ private DataFlow::SourceNode prototypeLessObject(DataFlow::TypeTracker t) {
119148
exists(DataFlow::TypeTracker t2 | result = prototypeLessObject(t2).track(t2, t))
120149
}
121150

151+
/**
152+
* A test file.
153+
* Objects created in such files are ignored in the `prototypeLessObject` predicate.
154+
*/
155+
private class TestFile extends File {
156+
TestFile() { ClassifyFiles::isTestFile(this) }
157+
}
158+
122159
/** Holds if `Object.prototype` has a member named `prop`. */
123160
private predicate isPropertyPresentOnObjectPrototype(string prop) {
124161
exists(ExternalInstanceMemberDecl decl |
@@ -215,3 +252,15 @@ private class EqualityCheck extends TaintTracking::SanitizerGuardNode, DataFlow:
215252
outcome = astNode.getPolarity().booleanNot()
216253
}
217254
}
255+
256+
/**
257+
* Sanitizer guard of the form `x.includes("__proto__")`.
258+
*/
259+
private class IncludesCheck extends TaintTracking::LabeledSanitizerGuardNode, InclusionTest {
260+
IncludesCheck() { this.getContainedNode().mayHaveStringValue("__proto__") }
261+
262+
override predicate sanitizes(boolean outcome, Expr e) {
263+
e = getContainerNode().asExpr() and
264+
outcome = getPolarity().booleanNot()
265+
}
266+
}

javascript/ql/lib/semmle/javascript/security/dataflow/UnsafeShellCommandConstructionCustomizations.qll

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ module UnsafeShellCommandConstruction {
5050
/**
5151
* A parameter of an exported function, seen as a source for shell command constructed from library input.
5252
*/
53-
class ExternalInputSource extends Source, DataFlow::ParameterNode {
53+
class ExternalInputSource extends Source, DataFlow::SourceNode {
5454
ExternalInputSource() {
5555
this = Exports::getALibraryInputParameter() and
5656
not (
5757
// looks to be on purpose.
58-
this.getName() = ["cmd", "command"]
58+
this.(DataFlow::ParameterNode).getName() = ["cmd", "command"]
5959
or
60-
this.getName().regexpMatch(".*(Cmd|Command)$") // ends with "Cmd" or "Command"
60+
this.(DataFlow::ParameterNode).getName().regexpMatch(".*(Cmd|Command)$") // ends with "Cmd" or "Command"
6161
)
6262
}
6363
}

javascript/ql/lib/semmle/javascript/security/performance/PolynomialReDoSCustomizations.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ module PolynomialReDoS {
124124
/**
125125
* A parameter of an exported function, seen as a source for polynomial-redos.
126126
*/
127-
class ExternalInputSource extends Source, DataFlow::ParameterNode {
127+
class ExternalInputSource extends Source, DataFlow::SourceNode {
128128
ExternalInputSource() { this = Exports::getALibraryInputParameter() }
129129

130130
override string getKind() { result = "library" }

javascript/ql/src/Security/CWE-915/PrototypePollutingAssignment.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
2424
where cfg.hasFlowPath(source, sink)
2525
select sink, source, sink,
2626
"This assignment may alter Object.prototype if a malicious '__proto__' string is injected from $@.",
27-
source.getNode(), "here"
27+
source.getNode(), source.getNode().(Source).describe()

0 commit comments

Comments
 (0)