Skip to content

Commit 2889e07

Browse files
author
Max Schaefer
committed
JavaScript: Add new query UnvalidatedDynamicMethodCall.
1 parent cf1e7cf commit 2889e07

14 files changed

+582
-0
lines changed

javascript/config/suites/javascript/security

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
+ semmlecode-javascript-queries/Security/CWE-640/HostHeaderPoisoningInEmailGeneration.ql: /Security/CWE/CWE-640
2727
+ semmlecode-javascript-queries/Security/CWE-643/XpathInjection.ql: /Security/CWE/CWE-643
2828
+ semmlecode-javascript-queries/Security/CWE-730/RegExpInjection.ql: /Security/CWE/CWE-730
29+
+ semmlecode-javascript-queries/Security/CWE-754/UnvalidatedDynamicMethodCall.ql: /Security/CWE/CWE-754
2930
+ semmlecode-javascript-queries/Security/CWE-770/MissingRateLimiting.ql: /Security/CWE/CWE-770
3031
+ semmlecode-javascript-queries/Security/CWE-776/XmlBomb.ql: /Security/CWE/CWE-776
3132
+ semmlecode-javascript-queries/Security/CWE-798/HardcodedCredentials.ql: /Security/CWE/CWE-798
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
JavaScript makes it easy to look up object properties dynamically at runtime. In particular, methods
9+
can be looked up by name and then called. However, if he method name is user controlled, an attacker
10+
could choose a name that makes the application invoke an unexpected method, which may cause a runtime
11+
exception. If this exception is not handled, it could be used to mount a denial-of-service attack.
12+
</p>
13+
<p>
14+
For example, there might not be a method of the given name or the result of the lookup might not be
15+
a function, which would cause the method call to throw a <code>TypeError</code> at runtime.
16+
</p>
17+
<p>
18+
Another, more subtle example is where the result of the lookup is a standard library method from
19+
<code>Object.prototype</code>, which most objects have on their prototype chain. Examples of such
20+
methods include <code>valueOf</code>, <code>hasOwnProperty</code> and <code>__defineSetter__</code>.
21+
If the method call passes the wrong number or kind of arguments to these methods, they will
22+
throw an exception.
23+
</p>
24+
</overview>
25+
26+
<recommendation>
27+
<p>
28+
It is best to avoid dynamic method lookup involving user-controlled names altogether, for instance
29+
by using a <code>Map</code> instead of a plain object.
30+
</p>
31+
<p>
32+
If the dynamic method lookup cannot be avoided, consider whitelisting permitted method names. At
33+
the very least, check that the method is an own property and not inherited from the prototype object.
34+
If the object on which the method is looked up contains properties that are not methods, you
35+
should additionally check that the result of the lookup is a function. Even if the object only
36+
contains methods it is still a good idea to perform this check in case other properties are
37+
added to the object later on.
38+
</p>
39+
</recommendation>
40+
41+
<example>
42+
<p>
43+
In the following example, an HTTP request parameter <code>action</code> property is used to dynamically
44+
look up a function in the <code>actions</code> map, which is then invoked with the <code>payload</code>
45+
parameter as its argument.
46+
</p>
47+
48+
<sample src="examples/UnvalidatedDynamicMethodCall.js" />
49+
50+
<p>
51+
The intention is to allow clients to invoke the <code>play</code> or <code>pause</code> method, but there
52+
is no check that <code>action</code> is actually the name of a method stored in <code>actions</code>.
53+
If, for example, <code>action</code> is <code>rewind</code>, <code>action</code> will be <code>undefined</code>
54+
and the call will result in a runtime error.
55+
</p>
56+
57+
<p>
58+
The easiest way to prevent this is to turn <code>actions</code> into a <code>Map</code> and using
59+
<code>Map.prototype.has</code> to check whether the method name is valid before looking it up.
60+
</p>
61+
62+
<sample src="examples/UnvalidatedDynamicMethodCallGood.js" />
63+
64+
<p>
65+
If <code>actions</code> cannot be turned into a <code>Map</code>, a <code>hasOwnProperty</code>
66+
check should be added to validate the method name:
67+
</p>
68+
69+
<sample src="examples/UnvalidatedDynamicMethodCallGood2.js" />
70+
71+
</example>
72+
73+
<references>
74+
<li>
75+
OWASP:
76+
<a href="https://www.owasp.org/index.php/Denial_of_Service">Denial of Service</a>.
77+
</li>
78+
<li>
79+
MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">Map</a>.
80+
</li>
81+
<li>
82+
MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype">Object.prototype</a>.
83+
</li>
84+
</references>
85+
86+
</qhelp>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name Unvalidated dynamic method call
3+
* @description Calling a method with a user-controlled name may dispatch to
4+
* an unexpected target, which could cause an exception.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @precision high
8+
* @id js/unvalidated-dynamic-method-call
9+
* @tags security
10+
* external/cwe/cwe-754
11+
*/
12+
13+
import javascript
14+
import semmle.javascript.security.dataflow.UnvalidatedDynamicMethodCall::UnvalidatedDynamicMethodCall
15+
import DataFlow::PathGraph
16+
17+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
18+
where cfg.hasFlowPath(source, sink)
19+
select sink.getNode(), source, sink,
20+
"Invocation of method with $@ name may dispatch to unexpected target and cause an exception.",
21+
source.getNode(), "user-controlled"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
var express = require('express');
2+
var app = express();
3+
4+
var actions = {
5+
play(data) {
6+
// ...
7+
},
8+
pause(data) {
9+
// ...
10+
}
11+
}
12+
13+
app.get('/perform/:action/:payload', function(req, res) {
14+
let action = actions[req.params.action];
15+
res.end(action(req.params.payload));
16+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
var express = require('express');
2+
var app = express();
3+
4+
var actions = new Map();
5+
actions.put("play", function (data) {
6+
// ...
7+
});
8+
actions.put("pause", function(data) {
9+
// ...
10+
});
11+
12+
app.get('/perform/:action/:payload', function(req, res) {
13+
if (actions.has(req.params.action)) {
14+
let action = actions.get(req.params.action);
15+
res.end(action(req.params.payload));
16+
} else {
17+
res.end("Unsupported action.");
18+
}
19+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
var express = require('express');
2+
var app = express();
3+
4+
var actions = {
5+
play(data) {
6+
// ...
7+
},
8+
pause(data) {
9+
// ...
10+
}
11+
}
12+
13+
app.get('/perform/:action/:payload', function(req, res) {
14+
if (actions.hasOwnProperty(req.params.action)) {
15+
let action = actions[req.params.action];
16+
if (typeof action === 'function') {
17+
res.end(action(req.params.payload));
18+
return;
19+
}
20+
}
21+
res.end("Unsupported action.");
22+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Provides a taint-tracking configuration for reasoning about unvalidated dynamic
3+
* method calls.
4+
*/
5+
6+
import javascript
7+
import semmle.javascript.frameworks.Express
8+
import PropertyInjectionShared
9+
private import semmle.javascript.dataflow.InferredTypes
10+
11+
module UnvalidatedDynamicMethodCall {
12+
private import DataFlow::FlowLabel
13+
14+
/**
15+
* A data flow source for unvalidated dynamic method calls.
16+
*/
17+
abstract class Source extends DataFlow::Node {
18+
/**
19+
* Gets the flow label relevant for this source.
20+
*/
21+
DataFlow::FlowLabel getFlowLabel() {
22+
result = data()
23+
}
24+
}
25+
26+
/**
27+
* A data flow sink for unvalidated dynamic method calls.
28+
*/
29+
abstract class Sink extends DataFlow::Node {
30+
/**
31+
* Gets the flow label relevant for this sink
32+
*/
33+
abstract DataFlow::FlowLabel getFlowLabel();
34+
}
35+
36+
/**
37+
* A sanitizer for unvalidated dynamic method calls.
38+
*/
39+
abstract class Sanitizer extends DataFlow::Node {
40+
abstract predicate sanitizes(DataFlow::Node source, DataFlow::Node sink, DataFlow::FlowLabel lbl);
41+
}
42+
43+
/**
44+
* A flow label describing values read from a user-controlled property that
45+
* may not be functions.
46+
*/
47+
private class MaybeNonFunction extends DataFlow::FlowLabel {
48+
MaybeNonFunction() { this = "MaybeNonFunction" }
49+
}
50+
51+
/**
52+
* A flow label describing values read from a user-controlled property that
53+
* may originate from a prototype object.
54+
*/
55+
private class MaybeFromProto extends DataFlow::FlowLabel {
56+
MaybeFromProto() { this = "MaybeFromProto" }
57+
}
58+
59+
/**
60+
* A taint-tracking configuration for reasoning about unvalidated dynamic method calls.
61+
*/
62+
class Configuration extends TaintTracking::Configuration {
63+
Configuration() { this = "UnvalidatedDynamicMethodCall" }
64+
65+
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
66+
source.(Source).getFlowLabel() = label
67+
}
68+
69+
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
70+
sink.(Sink).getFlowLabel() = label
71+
}
72+
73+
override predicate isSanitizer(DataFlow::Node nd) {
74+
super.isSanitizer(nd) or
75+
nd instanceof PropertyInjection::Sanitizer
76+
}
77+
78+
override predicate isAdditionalFlowStep(DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel, DataFlow::FlowLabel dstlabel) {
79+
exists (DataFlow::PropRead read |
80+
src = read.getPropertyNameExpr().flow() and
81+
dst = read and
82+
(srclabel = data() or srclabel = taint()) and
83+
(dstlabel instanceof MaybeNonFunction
84+
or
85+
// a property of `Object.create(null)` cannot come from a prototype
86+
not PropertyInjection::isPrototypeLessObject(read.getBase().getALocalSource()) and
87+
dstlabel instanceof MaybeFromProto) and
88+
// avoid overlapping results with unsafe dynamic method access query
89+
not PropertyInjection::hasUnsafeMethods(read.getBase().getALocalSource())
90+
)
91+
}
92+
}
93+
94+
/**
95+
* A source of remote user input, considered as a source for unvalidated dynamic method calls.
96+
*/
97+
class RemoteFlowSourceAsSource extends Source {
98+
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
99+
}
100+
101+
/**
102+
* The page URL considered as a flow source for unvalidated dynamic method calls.
103+
*/
104+
class DocumentUrlAsSource extends Source {
105+
DocumentUrlAsSource() { isDocumentURL(asExpr()) }
106+
}
107+
108+
/**
109+
* A function invocation of an unsafe function, as a sink for remote unvalidated dynamic method calls.
110+
*/
111+
class CalleeAsSink extends Sink {
112+
InvokeExpr invk;
113+
114+
CalleeAsSink() {
115+
this = invk.getCallee().flow() and
116+
// don't flag invocations inside a try-catch
117+
not invk.getASuccessor() instanceof CatchClause
118+
}
119+
120+
override DataFlow::FlowLabel getFlowLabel() {
121+
result instanceof MaybeNonFunction and
122+
// don't flag if the type inference can prove that it is a function;
123+
// this complements the `FunctionCheck` sanitizer below: the type inference can
124+
// detect more checks locally, but doesn't provide inter-procedural reasoning
125+
this.analyze().getAType() != TTFunction()
126+
or
127+
result instanceof MaybeFromProto
128+
}
129+
}
130+
131+
/**
132+
* A check of the form `typeof x === 'function'`, which sanitizes away the `MaybeNonFunction`
133+
* taint kind.
134+
*/
135+
class FunctionCheck extends TaintTracking::LabeledSanitizerGuardNode, DataFlow::ValueNode {
136+
override EqualityTest astNode;
137+
TypeofExpr t;
138+
139+
FunctionCheck() {
140+
astNode.getAnOperand().getStringValue() = "function" and
141+
astNode.getAnOperand().getUnderlyingValue() = t
142+
}
143+
144+
override predicate sanitizes(boolean outcome, Expr e) {
145+
outcome = astNode.getPolarity() and
146+
e = t.getOperand().getUnderlyingValue()
147+
}
148+
149+
override DataFlow::FlowLabel getALabel() {
150+
result instanceof MaybeNonFunction
151+
}
152+
}
153+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// copied from tests for `UnsafeDynamicMethodAccess.ql` to check that they do not overlap
2+
3+
let obj = {};
4+
5+
window.addEventListener('message', (ev) => {
6+
let message = JSON.parse(ev.data);
7+
window[message.name](message.payload); // NOT OK, but reported by UnsafeDynamicMethodAccess.ql
8+
new window[message.name](message.payload); // NOT OK, but reported by UnsafeDynamicMethodAccess.ql
9+
window["HTMLElement" + message.name](message.payload); // OK - concatenation restricts choice of methods
10+
window[`HTMLElement${message.name}`](message.payload); // OK - concatenation restricts choice of methods
11+
12+
function f() {}
13+
f[message.name](message.payload)(); // NOT OK, but reported by UnsafeDynamicMethodAccess.ql
14+
15+
obj[message.name](message.payload); // NOT OK
16+
17+
window[ev](ev); // NOT OK, but reported by UnsafeDynamicMethodAccess.ql
18+
});

0 commit comments

Comments
 (0)