Skip to content

Commit 591fcda

Browse files
committed
various improvements to the js/missing-origin-verification query
1 parent 2d6d304 commit 591fcda

File tree

7 files changed

+139
-69
lines changed

7 files changed

+139
-69
lines changed

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,31 @@
55

66
<overview>
77

8-
<p>If you use cross-origin communication between Window objects and do expect to receive messages from other sites, always verify the sender's identity using the origin and possibly source properties of the recevied `MessageEvent`. </p>
9-
10-
<p>Unexpected behaviours, like `DOM-based XSS` could occur, if the event handler for incoming data does not check the origin of the data received and handles the data in an unsafe way.</p>
8+
<p>
9+
The <code>"message"</code> event is used to send messages between windows.
10+
An untrusted origin is allowed to send messages to a trusted window, and if the origin
11+
is not checked that can lead to various security issues.
12+
</p>
1113
</overview>
1214

1315
<recommendation>
1416
<p>
15-
Always verify the sender's identity of incoming messages.
17+
Always verify the origin of incoming messages.
1618
</p>
17-
1819
</recommendation>
1920

2021
<example>
21-
<p>In the first example, the `MessageEvent.data` is passed to the `eval` function withouth checking the origin. This means that any window can send arbitrary messages that will be executed in the window receiving the message</p>
22+
<p>
23+
The example below uses a received message to execute some code. However, the
24+
origin of the message is not checked, so it might be possible for an attacker
25+
to execute arbitrary code.
26+
</p>
2227
<sample src="examples/postMessageNoOriginCheck.js" />
2328

24-
<p> In the second example, the `MessageEvent.origin` is verified with an unsecure check. For example, using `event.origin.indexOf('www.example.com') > -1` can be bypassed because the string `www.example.com` could appear anywhere in `event.origin` (i.e. `www.example.com.mydomain.com`)</p>
25-
<sample src="examples/postMessageInsufficientCheck.js" />
26-
27-
<p> In the third example, the `MessageEvent.origin` is properly checked against a trusted origin. </p>
29+
<p>
30+
The example is fixed below, where the origin is checked to be trusted.
31+
It is therefore not possible for an attacker to attack using an untrusted origin.
32+
</p>
2833
<sample src="examples/postMessageWithOriginCheck.js" />
2934

3035
</example>
Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,73 @@
11
/**
2-
* @name Missing `MessageEvent.origin` verification in `postMessage` handlers
3-
* @description Missing the `MessageEvent.origin` verification in `postMessage` handlers, allows any windows to send arbitrary data to the `MessageEvent` listener.
4-
* This could lead to unexpected behavior, especially when `MessageEvent.data` is used in an unsafe way.
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.
54
* @kind problem
65
* @problem.severity warning
7-
* @precision high
8-
* @id js/missing-postmessageorigin-verification
6+
* @security-severity 5
7+
* @precision medium
8+
* @id js/missing-origin-verification
99
* @tags correctness
1010
* security
1111
* external/cwe/cwe-020
1212
*/
1313

1414
import javascript
15-
import semmle.javascript.security.dataflow.DOM
1615

17-
/**
18-
* A method call for the insecure functions used to verify the `MessageEvent.origin`.
19-
*/
20-
class InsufficientOriginChecks extends DataFlow::Node {
21-
InsufficientOriginChecks() {
22-
exists(DataFlow::Node node |
23-
this.(StringOps::StartsWith).getSubstring() = node or
24-
this.(StringOps::Includes).getSubstring() = node or
25-
this.(StringOps::EndsWith).getSubstring() = node
26-
)
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())
2723
}
2824
}
2925

30-
/**
31-
* A function handler for the `MessageEvent`.
32-
*/
33-
class PostMessageHandler extends DataFlow::FunctionNode {
34-
PostMessageHandler() { this.getFunction() instanceof PostMessageEventHandler }
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))
3532
}
3633

37-
/**
38-
* The `MessageEvent` parameter received by the handler
39-
*/
40-
class PostMessageEvent extends DataFlow::SourceNode {
41-
PostMessageEvent() { exists(PostMessageHandler handler | this = handler.getParameter(0)) }
42-
43-
/**
44-
* Holds if an access on `MessageEvent.origin` is in an `EqualityTest` and there is no call of an insufficient verification method on `MessageEvent.origin`
45-
*/
46-
predicate hasOriginChecked() {
47-
exists(EqualityTest test |
48-
this.getAPropertyRead(["origin", "source"]).flowsToExpr(test.getAnOperand())
49-
)
50-
}
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+
}
5147

52-
/**
53-
* Holds if there is an insufficient method call (i.e indexOf) used to verify `MessageEvent.origin`
54-
*/
55-
predicate hasOriginInsufficientlyChecked() {
56-
this.getAPropertyRead("origin").getAMethodCall*() instanceof InsufficientOriginChecks
57-
}
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()))
5869
}
5970

60-
from PostMessageEvent event
61-
where not event.hasOriginChecked() or event.hasOriginInsufficientlyChecked()
62-
select event, "Missing or unsafe origin verification."
71+
from PostMessageHandler handler
72+
where not hasOriginCheck(handler)
73+
select handler.getEventParameter(), "Postmessage handler has no origin check."

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

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

javascript/ql/src/Security/CWE-020/examples/postMessageWithOriginCheck.js

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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
| tst.js:11:20:11:24 | event | Postmessage handler has no origin check. |
2+
| tst.js:24:27:24:27 | e | Postmessage handler has no origin check. |
3+
| tst.js:40:27:40:27 | e | Postmessage handler has no origin check. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-020/PostMessageNoOriginCheck.ql
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
window.onmessage = event => { // OK - good origin check
2+
let origin = event.origin.toLowerCase();
3+
4+
if (origin !== window.location.origin) {
5+
return;
6+
}
7+
8+
eval(event.data);
9+
}
10+
11+
window.onmessage = event => { // NOT OK - no origin check
12+
let origin = event.origin.toLowerCase();
13+
14+
console.log(origin);
15+
eval(event.data);
16+
}
17+
18+
window.onmessage = event => { // OK - there is an origin check
19+
if (event.origin === "https://www.example.com") {
20+
// do something
21+
}
22+
}
23+
24+
self.onmessage = function(e) { // NOT OK
25+
Commands[e.data.cmd].apply(null, e.data.args);
26+
};
27+
28+
window.onmessage = event => { // OK - there is an origin check
29+
if (mySet.includes(event.origin)) {
30+
// do something
31+
}
32+
}
33+
34+
window.onmessage = event => { // OK - there is an origin check
35+
if (mySet.includes(event.source)) {
36+
// do something
37+
}
38+
}
39+
40+
self.onmessage = function(e) { // NOT OK
41+
Commands[e.data.cmd].apply(null, e.data.args);
42+
};
43+
44+
window.addEventListener('message', function(e) { // OK - has a good origin check
45+
if (is_sysend_post_message(e) && is_valid_origin(e.origin)) {
46+
var payload = JSON.parse(e.data);
47+
if (payload && payload.name === uniq_prefix) {
48+
var data = unserialize(payload.data);
49+
sysend.broadcast(payload.key, data);
50+
}
51+
}
52+
});
53+
54+
function is_valid_origin(origin) {
55+
if (!domains) {
56+
warn("no domains configured");
57+
return true;
58+
}
59+
var valid = domains.includes(origin);
60+
if (!valid) {
61+
warn("invalid origin: " + origin);
62+
}
63+
return valid;
64+
}

0 commit comments

Comments
 (0)