Skip to content

Commit 7f3f629

Browse files
authored
Merge pull request #2913 from asger-semmle/js/prototype-pollution-path
Approved by erik-krogh
2 parents b3cbf8b + 52ebe49 commit 7f3f629

File tree

6 files changed

+375
-31
lines changed

6 files changed

+375
-31
lines changed

change-notes/1.24/analysis-javascript.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
| Regular expression always matches (`js/regex/always-matches`) | correctness, regular-expressions | Highlights regular expression checks that trivially succeed by matching an empty substring. Results are shown on LGTM by default. |
4545
| Missing await (`js/missing-await`) | correctness | Highlights expressions that operate directly on a promise object in a nonsensical way, instead of awaiting its result. Results are shown on LGTM by default. |
4646
| Polynomial regular expression used on uncontrolled data (`js/polynomial-redos`) | security, external/cwe/cwe-730, external/cwe/cwe-400 | Highlights expensive regular expressions that may be used on malicious input. Results are shown on LGTM by default. |
47-
| Prototype pollution in utility function (`js/prototype-pollution-utility`) | security, external/cwe/cwe-400, external/cwe/cwe-471 | Highlights recursive copying operations that are susceptible to prototype pollution. Results are shown on LGTM by default. |
47+
| Prototype pollution in utility function (`js/prototype-pollution-utility`) | security, external/cwe/cwe-400, external/cwe/cwe-471 | Highlights recursive assignment operations that are susceptible to prototype pollution. Results are shown on LGTM by default. |
4848
| Unsafe jQuery plugin (`js/unsafe-jquery-plugin`) | Highlights potential XSS vulnerabilities in unsafely designed jQuery plugins. Results are shown on LGTM by default. |
4949
| Unnecessary use of `cat` process (`js/unnecessary-use-of-cat`) | correctness, security, maintainability | Highlights command executions of `cat` where the fs API should be used instead. Results are shown on LGTM by default. |
5050

javascript/ql/src/Security/CWE-400/PrototypePollutionUtility.qhelp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
<p>
1515
One way to cause prototype pollution is through use of an unsafe <em>merge</em> or <em>extend</em> function
16-
to recursively copy properties from one object to another.
16+
to recursively copy properties from one object to another, or through the use of a <em>deep assignment</em>
17+
function to assign to an unverified chain of property names.
1718
Such a function has the potential to modify any object reachable from the destination object, and
1819
the built-in <code>Object.prototype</code> is usually reachable through the special properties
1920
<code>__proto__</code> and <code>constructor.prototype</code>.
@@ -23,13 +24,13 @@
2324
<recommendation>
2425
<p>
2526
The most effective place to guard against this is in the function that performs
26-
the recursive copy.
27+
the recursive copy or deep assignment.
2728
</p>
2829

2930
<p>
30-
Only merge a property recursively when it is an own property of the <em>destination</em> object.
31+
Only merge or assign a property recursively when it is an own property of the <em>destination</em> object.
3132
Alternatively, blacklist the property names <code>__proto__</code> and <code>constructor</code>
32-
from being merged.
33+
from being merged or assigned to.
3334
</p>
3435
</recommendation>
3536

javascript/ql/src/Security/CWE-400/PrototypePollutionUtility.ql

Lines changed: 133 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @name Prototype pollution in utility function
3-
* @description Recursively copying properties between objects may cause
4-
* accidental modification of a built-in prototype object.
3+
* @description Recursively assigning properties on objects may cause
4+
* accidental modification of a built-in prototype object.
55
* @kind path-problem
66
* @problem.severity warning
77
* @precision high
@@ -16,6 +16,81 @@ import DataFlow
1616
import PathGraph
1717
import semmle.javascript.DynamicPropertyAccess
1818

19+
/**
20+
* A call of form `x.split(".")` where `x` is a parameter.
21+
*
22+
* We restrict this to parameter nodes to focus on "deep assignment" functions.
23+
*/
24+
class SplitCall extends MethodCallNode {
25+
SplitCall() {
26+
getMethodName() = "split" and
27+
getArgument(0).mayHaveStringValue(".") and
28+
getReceiver().getALocalSource() instanceof ParameterNode
29+
}
30+
}
31+
32+
/**
33+
* Holds if `pred -> succ` should preserve polluted property names.
34+
*/
35+
predicate copyArrayStep(SourceNode pred, SourceNode succ) {
36+
// x -> [...x]
37+
exists(SpreadElement spread |
38+
pred.flowsTo(spread.getOperand().flow()) and
39+
succ.asExpr().(ArrayExpr).getAnElement() = spread
40+
)
41+
or
42+
// `x -> y` in `y.push( x[i] )`
43+
exists(MethodCallNode push |
44+
push = succ.getAMethodCall("push") and
45+
(
46+
getAnEnumeratedArrayElement(pred).flowsTo(push.getAnArgument())
47+
or
48+
pred.flowsTo(push.getASpreadArgument())
49+
)
50+
)
51+
or
52+
// x -> x.concat(...)
53+
exists(MethodCallNode concat_ |
54+
concat_.getMethodName() = "concat" and
55+
(pred = concat_.getReceiver() or pred = concat_.getAnArgument()) and
56+
succ = concat_
57+
)
58+
}
59+
60+
/**
61+
* Holds if `node` may refer to a `SplitCall` or a copy thereof, possibly
62+
* returned through a function call.
63+
*/
64+
predicate isSplitArray(SourceNode node) {
65+
node instanceof SplitCall
66+
or
67+
exists(SourceNode pred | isSplitArray(pred) |
68+
copyArrayStep(pred, node)
69+
or
70+
pred.flowsToExpr(node.(CallNode).getACallee().getAReturnedExpr())
71+
)
72+
}
73+
74+
/**
75+
* A property name originating from a `x.split(".")` call.
76+
*/
77+
class SplitPropName extends SourceNode {
78+
SourceNode array;
79+
80+
SplitPropName() {
81+
isSplitArray(array) and
82+
this = getAnEnumeratedArrayElement(array)
83+
}
84+
85+
/**
86+
* Gets the array from which this property name was obtained (the result from `split`).
87+
*/
88+
SourceNode getArray() { result = array }
89+
90+
/** Gets an element accessed on the same underlying array. */
91+
SplitPropName getAnAlias() { result.getArray() = getArray() }
92+
}
93+
1994
/**
2095
* Holds if the properties of `node` are enumerated locally.
2196
*/
@@ -24,13 +99,23 @@ predicate arePropertiesEnumerated(DataFlow::SourceNode node) {
2499
}
25100

26101
/**
27-
* Holds if `node` may flow from an enumerated prop name, possibly
28-
* into function calls (but not returns).
102+
* Holds if `node` is a source of property names that we consider possible
103+
* prototype pollution payloads.
29104
*/
30-
predicate isEnumeratedPropName(Node node) {
105+
predicate isPollutedPropNameSource(DataFlow::Node node) {
31106
node instanceof EnumeratedPropName
32107
or
33-
exists(Node pred | isEnumeratedPropName(pred) |
108+
node instanceof SplitPropName
109+
}
110+
111+
/**
112+
* Holds if `node` may flow from a source of polluted propery names, possibly
113+
* into function calls (but not returns).
114+
*/
115+
predicate isPollutedPropName(Node node) {
116+
isPollutedPropNameSource(node)
117+
or
118+
exists(Node pred | isPollutedPropName(pred) |
34119
node = pred.getASuccessor()
35120
or
36121
argumentPassingStep(_, pred, _, node)
@@ -51,7 +136,7 @@ predicate isEnumeratedPropName(Node node) {
51136
predicate isPotentiallyObjectPrototype(SourceNode node) {
52137
exists(Node base, Node key |
53138
dynamicPropReadStep(base, key, node) and
54-
isEnumeratedPropName(key) and
139+
isPollutedPropName(key) and
55140
// Ignore cases where the properties of `base` are enumerated, to avoid FPs
56141
// where the key came from that enumeration (and thus will not return Object.prototype).
57142
// For example, `src[key]` in `for (let key in src) { ... src[key] ... }` will generally
@@ -85,7 +170,13 @@ predicate dynamicPropWrite(DataFlow::Node base, DataFlow::Node prop, DataFlow::N
85170
// Prune writes that are unlikely to modify Object.prototype.
86171
// This is mainly for performance, but may block certain results due to
87172
// not tracking out of function returns and into callbacks.
88-
isPotentiallyObjectPrototype(base.getALocalSource())
173+
isPotentiallyObjectPrototype(base.getALocalSource()) and
174+
// Ignore writes with an obviously safe RHS.
175+
not exists(Expr e | e = rhs.asExpr() |
176+
e instanceof Literal or
177+
e instanceof ObjectExpr or
178+
e instanceof ArrayExpr
179+
)
89180
)
90181
}
91182

@@ -141,10 +232,10 @@ class PropNameTracking extends DataFlow::Configuration {
141232

142233
override predicate isSource(DataFlow::Node node, FlowLabel label) {
143234
label instanceof UnsafePropLabel and
144-
exists(EnumeratedPropName prop |
145-
node = prop
235+
(
236+
isPollutedPropNameSource(node)
146237
or
147-
node = prop.getASourceProp()
238+
node = any(EnumeratedPropName prop).getASourceProp()
148239
)
149240
}
150241

@@ -404,22 +495,29 @@ string deriveExprName(DataFlow::Node node) {
404495
result = getExprName(node)
405496
or
406497
not exists(getExprName(node)) and
407-
result = "this object"
498+
result = "here"
408499
}
409500

410501
/**
411502
* Holds if the dynamic property write `base[prop] = rhs` can pollute the prototype
412-
* of `base` due to flow from `enum`.
503+
* of `base` due to flow from `propNameSource`.
413504
*
414505
* In most cases this will result in an alert, the exception being the case where
415506
* `base` does not have a prototype at all.
416507
*/
417-
predicate isPrototypePollutingAssignment(Node base, Node prop, Node rhs, EnumeratedPropName enum) {
508+
predicate isPrototypePollutingAssignment(Node base, Node prop, Node rhs, Node propNameSource) {
418509
dynamicPropWrite(base, prop, rhs) and
510+
isPollutedPropNameSource(propNameSource) and
419511
exists(PropNameTracking cfg |
420-
cfg.hasFlow(enum, base) and
421-
cfg.hasFlow(enum, prop) and
422-
cfg.hasFlow(enum.getASourceProp(), rhs)
512+
cfg.hasFlow(propNameSource, base) and
513+
if propNameSource instanceof EnumeratedPropName
514+
then
515+
cfg.hasFlow(propNameSource, prop) and
516+
cfg.hasFlow(propNameSource.(EnumeratedPropName).getASourceProp(), rhs)
517+
else (
518+
cfg.hasFlow(propNameSource.(SplitPropName).getAnAlias(), prop) and
519+
rhs.getALocalSource() instanceof ParameterNode
520+
)
423521
)
424522
}
425523

@@ -464,18 +562,29 @@ class ObjectCreateNullCall extends CallNode {
464562
}
465563

466564
from
467-
PropNameTracking cfg, DataFlow::PathNode source, DataFlow::PathNode sink, EnumeratedPropName enum,
468-
Node base
565+
PropNameTracking cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Node prop, Node base,
566+
string msg, Node col1, Node col2
469567
where
568+
isPollutedPropName(prop) and
470569
cfg.hasFlowPath(source, sink) and
471-
isPrototypePollutingAssignment(base, _, _, enum) and
570+
isPrototypePollutingAssignment(base, _, _, prop) and
472571
sink.getNode() = base and
473-
source.getNode() = enum and
572+
source.getNode() = prop and
474573
(
475574
getANodeLeadingToBaseBase(base) instanceof ObjectLiteralNode
476575
or
477576
not getANodeLeadingToBaseBase(base) instanceof ObjectCreateNullCall
577+
) and
578+
// Generate different messages for deep merge and deep assign cases.
579+
if prop instanceof EnumeratedPropName
580+
then (
581+
col1 = prop.(EnumeratedPropName).getSourceObject() and
582+
col2 = base and
583+
msg = "Properties are copied from $@ to $@ without guarding against prototype pollution."
584+
) else (
585+
col1 = prop and
586+
col2 = base and
587+
msg =
588+
"The property chain $@ is recursively assigned to $@ without guarding against prototype pollution."
478589
)
479-
select base, source, sink,
480-
"Properties are copied from $@ to $@ without guarding against prototype pollution.",
481-
enum.getSourceObject(), deriveExprName(enum.getSourceObject()), base, deriveExprName(base)
590+
select base, source, sink, msg, col1, deriveExprName(col1), col2, deriveExprName(col2)

javascript/ql/src/semmle/javascript/DynamicPropertyAccess.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ private import semmle.javascript.dataflow.internal.FlowSteps
1111
* Gets a node that refers to an element of `array`, likely obtained
1212
* as a result of enumerating the elements of the array.
1313
*/
14-
private SourceNode getAnEnumeratedArrayElement(SourceNode array) {
14+
SourceNode getAnEnumeratedArrayElement(SourceNode array) {
1515
exists(MethodCallNode call, string name |
1616
call = array.getAMethodCall(name) and
1717
(name = "forEach" or name = "map") and

0 commit comments

Comments
 (0)