Skip to content

Commit 48ca4ae

Browse files
committed
JS: prototype pollution query template
1 parent ff25a3e commit 48ca4ae

File tree

5 files changed

+195
-0
lines changed

5 files changed

+195
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
| examples/RemotePropertyInjection.js:8:8:8:11 | prop | A $@ is used as a property name to write to. | examples/RemotePropertyInjection.js:7:13:7:36 | req.que ... trolled | user-provided value |
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Provides a taint-tracking configuration for tracking user-controlled objects flowing
3+
* into a vulnerable `extends` call.
4+
*/
5+
6+
import javascript
7+
import semmle.javascript.security.TaintedObject
8+
9+
module PrototypePollution {
10+
/**
11+
* Label for wrappers around tainted objects, that is, objects that are
12+
* not completely user-controlled, but contain a user-controlled object.
13+
*
14+
* For example, `options` below is is a tainted wrapper, but is not itself
15+
* a tainted object:
16+
* ```
17+
* let options = {
18+
* prefs: {
19+
* locale: req.query.locale
20+
* }
21+
* }
22+
* ```
23+
*/
24+
module TaintedObjectWrapper {
25+
private class TaintedObjectWrapper extends DataFlow::FlowLabel {
26+
TaintedObjectWrapper() { this = "tainted-object-wrapper" }
27+
}
28+
29+
TaintedObjectWrapper label() { any() }
30+
}
31+
32+
/**
33+
* A data flow source for prototype pollution.
34+
*/
35+
abstract class Source extends DataFlow::Node {
36+
/**
37+
* Gets the type of data coming from this source.
38+
*/
39+
abstract DataFlow::FlowLabel getAFlowLabel();
40+
}
41+
42+
/**
43+
* A data flow sink for prototype pollution.
44+
*/
45+
abstract class Sink extends DataFlow::Node {
46+
/**
47+
* Gets the type of data that can taint this sink.
48+
*/
49+
abstract DataFlow::FlowLabel getAFlowLabel();
50+
}
51+
52+
/**
53+
* A taint tracking configuration for user-controlled objects flowing into deep `extend` calls,
54+
* leading to prototype pollution.
55+
*/
56+
class Configuration extends TaintTracking::Configuration {
57+
Configuration() { this = "PrototypePollution" }
58+
59+
override predicate isSource(DataFlow::Node node, DataFlow::FlowLabel label) {
60+
node.(Source).getAFlowLabel() = label
61+
}
62+
63+
override predicate isSink(DataFlow::Node node, DataFlow::FlowLabel label) {
64+
node.(Sink).getAFlowLabel() = label
65+
}
66+
67+
override predicate isAdditionalFlowStep(
68+
DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl
69+
) {
70+
TaintedObject::step(src, dst, inlbl, outlbl)
71+
or
72+
// Track objects are wrapped in other objects
73+
exists(DataFlow::PropWrite write |
74+
src = write.getRhs() and
75+
inlbl = TaintedObject::label() and
76+
dst = write.getBase().getALocalSource() and
77+
outlbl = TaintedObjectWrapper::label()
78+
)
79+
}
80+
81+
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode node) {
82+
node instanceof TaintedObject::SanitizerGuard
83+
}
84+
}
85+
86+
/**
87+
* A user-controlled string value, as a source of prototype pollution.
88+
*
89+
* Note that values from this type of source will need to flow through a `JSON.parse` call
90+
* in order to be flagged for prototype pollution.
91+
*/
92+
private class RemoteFlowAsSource extends Source {
93+
RemoteFlowAsSource() { this instanceof RemoteFlowSource }
94+
95+
override DataFlow::FlowLabel getAFlowLabel() { result = DataFlow::FlowLabel::data() }
96+
}
97+
98+
/**
99+
* A source of user-controlled objects.
100+
*/
101+
private class TaintedObjectSource extends Source {
102+
TaintedObjectSource() { this instanceof TaintedObject::Source }
103+
104+
override DataFlow::FlowLabel getAFlowLabel() { result = TaintedObject::label() }
105+
}
106+
107+
string getModuleName(ExtendCall call) {
108+
call = DataFlow::moduleImport(result).getACall() or
109+
call = DataFlow::moduleMember(result, _).getACall()
110+
}
111+
112+
class DeepExtendSink extends Sink {
113+
ExtendCall call;
114+
115+
DeepExtendSink() {
116+
this = call.getASourceOperand() and
117+
call.isDeep() and
118+
exists(string moduleName | moduleName = getModuleName(call) |
119+
moduleName = "lodash" + any(string s) or
120+
moduleName = "just-extend" or
121+
moduleName = "extend" or
122+
moduleName = "extend2" or
123+
moduleName = "node.extend" or
124+
moduleName = "merge" or
125+
moduleName = "smart-extend" or
126+
moduleName = "js-extend" or
127+
moduleName = "deep" or
128+
moduleName = "defaults-deep"
129+
)
130+
}
131+
132+
override DataFlow::FlowLabel getAFlowLabel() {
133+
result = TaintedObject::label()
134+
or
135+
result = TaintedObjectWrapper::label()
136+
}
137+
}
138+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
nodes
2+
| PrototypePollution.js:7:17:7:29 | req.query.foo |
3+
| PrototypePollution.js:10:17:12:5 | {\\n ... K\\n } |
4+
| PrototypePollution.js:11:16:11:30 | req.query.value |
5+
| PrototypePollution.js:15:14:15:28 | req.query.value |
6+
| PrototypePollution.js:17:17:19:5 | {\\n ... K\\n } |
7+
| PrototypePollution.js:18:16:18:25 | opts.thing |
8+
edges
9+
| PrototypePollution.js:11:16:11:30 | req.query.value | PrototypePollution.js:10:17:12:5 | {\\n ... K\\n } |
10+
| PrototypePollution.js:15:14:15:28 | req.query.value | PrototypePollution.js:18:16:18:25 | opts.thing |
11+
| PrototypePollution.js:18:16:18:25 | opts.thing | PrototypePollution.js:17:17:19:5 | {\\n ... K\\n } |
12+
#select
13+
| PrototypePollution.js:7:17:7:29 | req.query.foo | PrototypePollution.js:7:17:7:29 | req.query.foo | PrototypePollution.js:7:17:7:29 | req.query.foo | Prototype pollution caused by merging a user-controlled value from $@. | PrototypePollution.js:7:17:7:29 | req.query.foo | here |
14+
| PrototypePollution.js:10:17:12:5 | {\\n ... K\\n } | PrototypePollution.js:11:16:11:30 | req.query.value | PrototypePollution.js:10:17:12:5 | {\\n ... K\\n } | Prototype pollution caused by merging a user-controlled value from $@. | PrototypePollution.js:11:16:11:30 | req.query.value | here |
15+
| PrototypePollution.js:17:17:19:5 | {\\n ... K\\n } | PrototypePollution.js:15:14:15:28 | req.query.value | PrototypePollution.js:17:17:19:5 | {\\n ... K\\n } | Prototype pollution caused by merging a user-controlled value from $@. | PrototypePollution.js:15:14:15:28 | req.query.value | here |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
let express = require('express');
2+
let _ = require('lodash');
3+
4+
let app = express();
5+
6+
app.get('/hello', function(req, res) {
7+
_.merge({}, req.query.foo); // NOT OK
8+
_.merge({}, req.query); // NOT OK - but not flagged
9+
10+
_.merge({}, {
11+
value: req.query.value // NOT OK
12+
});
13+
14+
let opts = {
15+
thing: req.query.value // wrapped and unwrapped value
16+
};
17+
_.merge({}, {
18+
value: opts.thing // NOT OK
19+
});
20+
});
21+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name Prototype Pollution
3+
* @description Recursively merging a user-controlled object into another object
4+
* can allow an attacker to modify the built-in Object prototype.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @precision high
8+
* @id js/prototype-pollution
9+
* @tags security
10+
* external/cwe/cwe-250
11+
* external/cwe/cwe-400
12+
*/
13+
14+
import javascript
15+
import semmle.javascript.security.dataflow.PrototypePollution::PrototypePollution
16+
import DataFlow::PathGraph
17+
18+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where cfg.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink, "Prototype pollution caused by merging a user-controlled value from $@.", source, "here"

0 commit comments

Comments
 (0)