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
1616import PathGraph
1717import 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) {
51136predicate 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
466564from
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
469567where
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 )
0 commit comments