Skip to content

Commit 53b26eb

Browse files
authored
Merge pull request #8724 from erik-krogh/postMessage
JS: promote the `js/missing-origin-verification` query
2 parents fe1e47b + 58db922 commit 53b26eb

File tree

40 files changed

+231
-139
lines changed

40 files changed

+231
-139
lines changed

javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,19 +1225,25 @@ module TaintTracking {
12251225
* An equality test on `e.origin` or `e.source` where `e` is a `postMessage` event object,
12261226
* considered as a sanitizer for `e`.
12271227
*/
1228-
private class PostMessageEventSanitizer extends AdditionalSanitizerGuardNode, DataFlow::ValueNode {
1228+
private class PostMessageEventSanitizer extends AdditionalSanitizerGuardNode {
12291229
VarAccess event;
1230-
override EqualityTest astNode;
1230+
boolean polarity;
12311231

12321232
PostMessageEventSanitizer() {
1233-
exists(string prop | prop = "origin" or prop = "source" |
1234-
astNode.getAnOperand().(PropAccess).accesses(event, prop) and
1235-
event.mayReferToParameter(any(PostMessageEventHandler h).getEventParameter())
1233+
event.mayReferToParameter(any(PostMessageEventHandler h).getEventParameter()) and
1234+
exists(DataFlow::PropRead read | read.accesses(event.flow(), ["origin", "source"]) |
1235+
exists(EqualityTest test | polarity = test.getPolarity() and this.getAstNode() = test |
1236+
test.getAnOperand().flow() = read
1237+
)
1238+
or
1239+
exists(InclusionTest test | polarity = test.getPolarity() and this = test |
1240+
test.getContainedNode() = read
1241+
)
12361242
)
12371243
}
12381244

12391245
override predicate sanitizes(boolean outcome, Expr e) {
1240-
outcome = astNode.getPolarity() and
1246+
outcome = polarity and
12411247
e = event
12421248
}
12431249

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
8+
<p>
9+
The <code>"message"</code> event is used to send messages between windows.
10+
An untrusted window can send a message to a trusted window, and it is up to the receiver to verify the legitimacy of the message. One way of performing that verification is to check the <code>origin</code> of the message ensure that it originates from a trusted window.
11+
</p>
12+
</overview>
13+
14+
<recommendation>
15+
<p>
16+
Always verify the origin of incoming messages.
17+
</p>
18+
</recommendation>
19+
20+
<example>
21+
<p>
22+
The example below uses a received message to execute some code. However, the
23+
origin of the message is not checked, so it might be possible for an attacker
24+
to execute arbitrary code.
25+
</p>
26+
<sample src="examples/MissingOriginCheckBad.js" />
27+
28+
<p>
29+
The example is fixed below, where the origin is checked to be trusted.
30+
It is therefore not possible for a malicious user to perform an attack using an untrusted origin.
31+
</p>
32+
<sample src="examples/MissingOriginCheckGood.js" />
33+
34+
</example>
35+
36+
<references>
37+
38+
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">Window.postMessage()</a>.</li>
39+
<li><a href="https://portswigger.net/web-security/dom-based/web-message-manipulation">Web message manipulation</a>.</li>
40+
<li><a href="https://labs.detectify.com/2016/12/08/the-pitfalls-of-postmessage/">The pitfalls of postMessage</a>.</li>
41+
42+
</references>
43+
</qhelp>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @name Missing origin verification in `postMessage` handler
3+
* @description Missing origin verification in a `postMessage` handler allows any windows to send arbitrary data to the handler.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @security-severity 5
7+
* @precision medium
8+
* @id js/missing-origin-check
9+
* @tags correctness
10+
* security
11+
* external/cwe/cwe-020
12+
*/
13+
14+
import javascript
15+
16+
/** A function that handles "message" events. */
17+
class PostMessageHandler extends DataFlow::FunctionNode {
18+
override PostMessageEventHandler astNode;
19+
20+
/** Gets the parameter that contains the event. */
21+
DataFlow::ParameterNode getEventParameter() {
22+
result = DataFlow::parameterNode(astNode.getEventParameter())
23+
}
24+
}
25+
26+
/** Gets a reference to the event from a postmessage `handler` */
27+
DataFlow::SourceNode event(DataFlow::TypeTracker t, PostMessageHandler handler) {
28+
t.start() and
29+
result = handler.getEventParameter()
30+
or
31+
exists(DataFlow::TypeTracker t2 | result = event(t2, handler).track(t2, t))
32+
}
33+
34+
/** Gets a reference to the .origin from a postmessage event. */
35+
DataFlow::SourceNode origin(DataFlow::TypeTracker t, PostMessageHandler handler) {
36+
t.start() and
37+
result = event(DataFlow::TypeTracker::end(), handler).getAPropertyRead("origin")
38+
or
39+
result =
40+
origin(t.continue(), handler)
41+
.getAMethodCall([
42+
"toString", "toLowerCase", "toUpperCase", "toLocaleLowerCase", "toLocaleUpperCase"
43+
])
44+
or
45+
exists(DataFlow::TypeTracker t2 | result = origin(t2, handler).track(t2, t))
46+
}
47+
48+
/** Gets a reference to the .source from a postmessage event. */
49+
DataFlow::SourceNode source(DataFlow::TypeTracker t, PostMessageHandler handler) {
50+
t.start() and
51+
result = event(DataFlow::TypeTracker::end(), handler).getAPropertyRead("source")
52+
or
53+
exists(DataFlow::TypeTracker t2 | result = source(t2, handler).track(t2, t))
54+
}
55+
56+
/** Gets a reference to the origin or the source of a postmessage event. */
57+
DataFlow::SourceNode sourceOrOrigin(PostMessageHandler handler) {
58+
result = source(DataFlow::TypeTracker::end(), handler) or
59+
result = origin(DataFlow::TypeTracker::end(), handler)
60+
}
61+
62+
/** Holds if there exists a check of the .origin or .source of the postmessage `handler`. */
63+
predicate hasOriginCheck(PostMessageHandler handler) {
64+
// event.origin === "constant"
65+
exists(EqualityTest test | sourceOrOrigin(handler).flowsToExpr(test.getAnOperand()))
66+
or
67+
// set.includes(event.source)
68+
exists(InclusionTest test | sourceOrOrigin(handler).flowsTo(test.getContainedNode()))
69+
or
70+
// "safeOrigin".startsWith(event.origin)
71+
exists(StringOps::StartsWith starts |
72+
origin(DataFlow::TypeTracker::end(), handler).flowsTo(starts.getSubstring())
73+
)
74+
or
75+
// "safeOrigin".endsWith(event.origin)
76+
exists(StringOps::EndsWith ends |
77+
origin(DataFlow::TypeTracker::end(), handler).flowsTo(ends.getSubstring())
78+
)
79+
}
80+
81+
from PostMessageHandler handler
82+
where not hasOriginCheck(handler)
83+
select handler.getEventParameter(), "Postmessage handler has no origin check."
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
function postMessageHandler(event) {
22
console.log(event.origin)
33
// GOOD: the origin property is checked
4-
if (event.origin === 'www.example.com') {
4+
if (event.origin === 'https://www.example.com') {
55
// do something
66
}
77
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: newQuery
3+
---
4+
* The `js/missing-origin-check` query has been added. It highlights "message" event handlers that do not check the origin of the event.
5+
The query previously existed as the experimental `js/missing-postmessageorigin-verification` query.

javascript/ql/src/experimental/Security/CWE-020/PostMessageNoOriginCheck.qhelp

Lines changed: 0 additions & 40 deletions
This file was deleted.

javascript/ql/src/experimental/Security/CWE-020/PostMessageNoOriginCheck.ql

Lines changed: 0 additions & 62 deletions
This file was deleted.

javascript/ql/src/experimental/Security/CWE-020/examples/postMessageInsufficientCheck.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)