Skip to content

Commit 8e54c7a

Browse files
author
Max Schaefer
authored
Merge pull request #503 from asger-semmle/unsafe-global-object-access
JS: add method name injection query
2 parents 7dc0a81 + 61ef655 commit 8e54c7a

File tree

22 files changed

+369
-19
lines changed

22 files changed

+369
-19
lines changed

change-notes/1.19/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
| Enabling Node.js integration for Electron web content renderers (`js/enabling-electron-renderer-node-integration`) | security, frameworks/electron, external/cwe/cwe-094 | Highlights Electron web content renderer preferences with Node.js integration enabled, indicating a violation of [CWE-94](https://cwe.mitre.org/data/definitions/94.html). Results are not shown on LGTM by default. |
2525
| File data in outbound network request | security, external/cwe/cwe-200 | Highlights locations where file data is sent in a network request. Results are not shown on LGTM by default. |
2626
| Host header poisoning in email generation | security, external/cwe/cwe-640 | Highlights code that generates emails with links that can be hijacked by HTTP host header poisoning, indicating a violation of [CWE-640](https://cwe.mitre.org/data/definitions/640.html). Results shown on LGTM by default. |
27+
| Unsafe dynamic method access (`js/unsafe-dynamic-method-access` ) | security, external/cwe/cwe-094 | Highlights code that invokes a user-controlled method on an object with unsafe methods. Results are shown on LGTM by default. |
2728
| Replacement of a substring with itself (`js/identity-replacement`) | correctness, security, external/cwe/cwe-116 | Highlights string replacements that replace a string with itself, which usually indicates a mistake. Results shown on LGTM by default. |
2829
| Stored cross-site scripting (`js/stored-xss`) | security, external/cwe/cwe-079, external/cwe/cwe-116 | Highlights uncontrolled stored values flowing into HTML content, indicating a violation of [CWE-079](https://cwe.mitre.org/data/definitions/79.html). Results shown on LGTM by default. |
2930
| Unclear precedence of nested operators (`js/unclear-operator-precedence`) | maintainability, correctness, external/cwe/cwe-783 | Highlights nested binary operators whose relative precedence is easy to misunderstand. Results shown on LGTM by default. |

javascript/config/suites/javascript/security

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
+ semmlecode-javascript-queries/Security/CWE-079/Xss.ql: /Security/CWE/CWE-079
88
+ semmlecode-javascript-queries/Security/CWE-089/SqlInjection.ql: /Security/CWE/CWE-089
99
+ semmlecode-javascript-queries/Security/CWE-094/CodeInjection.ql: /Security/CWE/CWE-094
10+
+ semmlecode-javascript-queries/Security/CWE-094/UnsafeDynamicMethodAccess.ql: /Security/CWE/CWE-094
1011
+ semmlecode-javascript-queries/Security/CWE-116/IncompleteSanitization.ql: /Security/CWE/CWE-116
1112
+ semmlecode-javascript-queries/Security/CWE-134/TaintedFormatString.ql: /Security/CWE/CWE-134
1213
+ semmlecode-javascript-queries/Security/CWE-209/StackTraceExposure.ql: /Security/CWE/CWE-209
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Calling a user-controlled method on certain objects can lead to invocation of unsafe functions,
9+
such as <code>eval</code> or the <code>Function</code> constructor. In particular, the global object
10+
contains the <code>eval</code> function, and any function object contains the <code>Function</code> constructor
11+
in its <code>constructor</code> property.
12+
</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>
17+
Avoid invoking user-controlled methods on the global object or on any function object.
18+
Whitelist the permitted method names or change the type of object the methods are stored on.
19+
</p>
20+
</recommendation>
21+
22+
<example>
23+
<p>
24+
In the following example, a message from the document's parent frame can invoke the <code>play</code>
25+
or <code>pause</code> method. However, it can also invoke <code>eval</code>.
26+
A malicious website could embed the page in an iframe and execute arbitrary code by sending a message
27+
with the name <code>eval</code>.
28+
</p>
29+
30+
<sample src="examples/UnsafeDynamicMethodAccess.js" />
31+
32+
<p>
33+
Instead of storing the API methods in the global scope, put them in an API object or Map. It is also good
34+
practice to prevent invocation of inherited methods like <code>toString</code> and <code>valueOf</code>.
35+
</p>
36+
37+
<sample src="examples/UnsafeDynamicMethodAccessGood.js" />
38+
39+
</example>
40+
41+
<references>
42+
<li>
43+
OWASP:
44+
<a href="https://www.owasp.org/index.php/Code_Injection">Code Injection</a>.
45+
</li>
46+
<li>
47+
MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects#Function_properties">Global functions</a>.
48+
</li>
49+
<li>
50+
MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function">Function constructor</a>.
51+
</li>
52+
</references>
53+
</qhelp>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @name Unsafe dynamic method access
3+
* @description Invoking user-controlled methods on certain objects can lead to remote code execution.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @precision high
7+
* @id js/unsafe-dynamic-method-access
8+
* @tags security
9+
* external/cwe/cwe-094
10+
*/
11+
import javascript
12+
import semmle.javascript.security.dataflow.UnsafeDynamicMethodAccess::UnsafeDynamicMethodAccess
13+
import DataFlow::PathGraph
14+
15+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
16+
where cfg.hasFlowPath(source, sink)
17+
select sink, source, sink, "Invocation of method derived from $@ may lead to remote code execution.", source.getNode(), "user-controlled value"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// API methods
2+
function play(data) {
3+
// ...
4+
}
5+
function pause(data) {
6+
// ...
7+
}
8+
9+
window.addEventListener("message", (ev) => {
10+
let message = JSON.parse(ev.data);
11+
12+
// Let the parent frame call the 'play' or 'pause' function
13+
window[message.name](message.payload);
14+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// API methods
2+
let api = {
3+
play: function(data) {
4+
// ...
5+
},
6+
pause: function(data) {
7+
// ...
8+
}
9+
};
10+
11+
window.addEventListener("message", (ev) => {
12+
let message = JSON.parse(ev.data);
13+
14+
// Let the parent frame call the 'play' or 'pause' function
15+
if (!api.hasOwnProperty(message.name)) {
16+
return;
17+
}
18+
api[message.name](message.payload);
19+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Provides predicates for reasoning about flow of user-controlled values that are used
3+
* as property names.
4+
*/
5+
import javascript
6+
7+
module PropertyInjection {
8+
/**
9+
* A data-flow node that sanitizes user-controlled property names that flow through it.
10+
*/
11+
abstract class Sanitizer extends DataFlow::Node {
12+
}
13+
14+
/**
15+
* Concatenation with a constant, acting as a sanitizer.
16+
*/
17+
private class ConcatSanitizer extends Sanitizer {
18+
ConcatSanitizer() {
19+
StringConcatenation::getAnOperand(this).asExpr() instanceof ConstantString
20+
}
21+
}
22+
23+
/**
24+
* Holds if the methods of the given value are unsafe, such as `eval`.
25+
*/
26+
predicate hasUnsafeMethods(DataFlow::SourceNode node) {
27+
// eval and friends can be accessed from the global object.
28+
node = DataFlow::globalObjectRef()
29+
or
30+
// document.write can be accessed
31+
isDocument(node.asExpr())
32+
or
33+
// 'constructor' property leads to the Function constructor.
34+
node.analyze().getAValue() instanceof AbstractCallable
35+
or
36+
// Assume that a value that is invoked can refer to a function.
37+
exists (node.getAnInvocation())
38+
}
39+
}

javascript/ql/src/semmle/javascript/security/dataflow/RemotePropertyInjection.qll

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import javascript
88
import semmle.javascript.frameworks.Express
9+
import PropertyInjectionShared
910

1011
module RemotePropertyInjection {
1112
/**
@@ -45,7 +46,8 @@ module RemotePropertyInjection {
4546

4647
override predicate isSanitizer(DataFlow::Node node) {
4748
super.isSanitizer(node) or
48-
node instanceof Sanitizer
49+
node instanceof Sanitizer or
50+
node instanceof PropertyInjection::Sanitizer
4951
}
5052
}
5153

@@ -76,9 +78,12 @@ module RemotePropertyInjection {
7678
*/
7779
class MethodCallSink extends Sink, DataFlow::ValueNode {
7880
MethodCallSink() {
79-
exists (DataFlow::PropRead pr | astNode = pr.getPropertyNameExpr() |
80-
exists (pr.getAnInvocation())
81-
)
81+
exists (DataFlow::PropRead pr | astNode = pr.getPropertyNameExpr() |
82+
exists (pr.getAnInvocation()) and
83+
84+
// Omit sinks covered by the UnsafeDynamicMethodAccess query
85+
not PropertyInjection::hasUnsafeMethods(pr.getBase().getALocalSource())
86+
)
8287
}
8388

8489
override string getMessage() {
@@ -105,18 +110,4 @@ module RemotePropertyInjection {
105110
result = " a header name."
106111
}
107112
}
108-
109-
/**
110-
* A binary expression that sanitzes a value for remote property injection. That
111-
* is, if a string is prepended or appended to the remote input, an attacker
112-
* cannot access arbitrary properties.
113-
*/
114-
class ConcatSanitizer extends Sanitizer, DataFlow::ValueNode {
115-
116-
override BinaryExpr astNode;
117-
118-
ConcatSanitizer() {
119-
astNode.getAnOperand() instanceof ConstantString
120-
}
121-
}
122113
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Provides a taint-tracking configuration for reasoning about method invocations
3+
* with a user-controlled method name on objects with unsafe methods.
4+
*/
5+
6+
import javascript
7+
import semmle.javascript.frameworks.Express
8+
import PropertyInjectionShared
9+
10+
module UnsafeDynamicMethodAccess {
11+
private import DataFlow::FlowLabel
12+
13+
/**
14+
* A data flow source for unsafe dynamic method access.
15+
*/
16+
abstract class Source extends DataFlow::Node {
17+
/**
18+
* Gets the flow label relevant for this source.
19+
*/
20+
DataFlow::FlowLabel getFlowLabel() {
21+
result = data()
22+
}
23+
}
24+
25+
/**
26+
* A data flow sink for unsafe dynamic method access.
27+
*/
28+
abstract class Sink extends DataFlow::Node {
29+
/**
30+
* Gets the flow label relevant for this sink
31+
*/
32+
abstract DataFlow::FlowLabel getFlowLabel();
33+
}
34+
35+
/**
36+
* A sanitizer for unsafe dynamic method access.
37+
*/
38+
abstract class Sanitizer extends DataFlow::Node { }
39+
40+
/**
41+
* Gets the flow label describing values that may refer to an unsafe
42+
* function as a result of an attacker-controlled property name.
43+
*/
44+
UnsafeFunction unsafeFunction() { any() }
45+
private class UnsafeFunction extends DataFlow::FlowLabel {
46+
UnsafeFunction() { this = "UnsafeFunction" }
47+
}
48+
49+
/**
50+
* A taint-tracking configuration for reasoning about unsafe dynamic method access.
51+
*/
52+
class Configuration extends TaintTracking::Configuration {
53+
Configuration() { this = "UnsafeDynamicMethodAccess" }
54+
55+
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
56+
source.(Source).getFlowLabel() = label
57+
}
58+
59+
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
60+
sink.(Sink).getFlowLabel() = label
61+
}
62+
63+
override predicate isSanitizer(DataFlow::Node node) {
64+
super.isSanitizer(node) or
65+
node instanceof Sanitizer or
66+
node instanceof PropertyInjection::Sanitizer
67+
}
68+
69+
/**
70+
* Holds if a property of the given object is an unsafe function.
71+
*/
72+
predicate hasUnsafeMethods(DataFlow::SourceNode node) {
73+
PropertyInjection::hasUnsafeMethods(node) // Redefined here so custom queries can override it
74+
}
75+
76+
/**
77+
* Holds if the `node` is of form `Object.create(null)` and so it has no prototype.
78+
*/
79+
predicate isPrototypeLessObject(DataFlow::MethodCallNode node) {
80+
node = DataFlow::globalVarRef("Object").getAMethodCall("create") and
81+
node.getArgument(0).asExpr() instanceof NullLiteral
82+
}
83+
84+
override predicate isAdditionalFlowStep(DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel, DataFlow::FlowLabel dstlabel) {
85+
// Reading a property of the global object or of a function
86+
exists (DataFlow::PropRead read |
87+
hasUnsafeMethods(read.getBase().getALocalSource()) and
88+
src = read.getPropertyNameExpr().flow() and
89+
dst = read and
90+
(srclabel = data() or srclabel = taint()) and
91+
dstlabel = unsafeFunction())
92+
or
93+
// Reading a chain of properties from any object with a prototype can lead to Function
94+
exists (PropertyProjection proj |
95+
not isPrototypeLessObject(proj.getObject().getALocalSource()) and
96+
src = proj.getASelector() and
97+
dst = proj and
98+
(srclabel = data() or srclabel = taint()) and
99+
dstlabel = unsafeFunction())
100+
}
101+
}
102+
103+
/**
104+
* A source of remote user input, considered as a source for unsafe dynamic method access.
105+
*/
106+
class RemoteFlowSourceAsSource extends Source {
107+
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
108+
}
109+
110+
/**
111+
* The page URL considered as a flow source for unsafe dynamic method access.
112+
*/
113+
class DocumentUrlAsSource extends Source {
114+
DocumentUrlAsSource() { isDocumentURL(asExpr()) }
115+
}
116+
117+
/**
118+
* A function invocation of an unsafe function, as a sink for remote unsafe dynamic method access.
119+
*/
120+
class CalleeAsSink extends Sink {
121+
CalleeAsSink() {
122+
this = any(DataFlow::InvokeNode node).getCalleeNode()
123+
}
124+
125+
override DataFlow::FlowLabel getFlowLabel() {
126+
result = unsafeFunction()
127+
}
128+
}
129+
}

javascript/ql/test/query-tests/Security/CWE-094/CodeInjection.expected renamed to javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected

File renamed without changes.

0 commit comments

Comments
 (0)