-
Notifications
You must be signed in to change notification settings - Fork 2k
JS: promote the js/missing-origin-verification query
#8724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
18532ba
ec9c308
e2badab
2d6d304
591fcda
bca4d14
df295e6
fe3d71e
0a26e89
2d7c7ff
58db922
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <!DOCTYPE qhelp PUBLIC | ||
| "-//Semmle//qhelp//EN" | ||
| "qhelp.dtd"> | ||
| <qhelp> | ||
|
|
||
| <overview> | ||
|
|
||
| <p> | ||
| The <code>"message"</code> event is used to send messages between windows. | ||
| 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. | ||
| </p> | ||
| </overview> | ||
|
|
||
| <recommendation> | ||
| <p> | ||
| Always verify the origin of incoming messages. | ||
| </p> | ||
| </recommendation> | ||
|
|
||
| <example> | ||
| <p> | ||
| The example below uses a received message to execute some code. However, the | ||
| origin of the message is not checked, so it might be possible for an attacker | ||
| to execute arbitrary code. | ||
| </p> | ||
| <sample src="examples/MissingOriginCheckBad.js" /> | ||
|
|
||
| <p> | ||
| The example is fixed below, where the origin is checked to be trusted. | ||
| It is therefore not possible for a malicious user to perform an attack using an untrusted origin. | ||
| </p> | ||
| <sample src="examples/MissingOriginCheckGood.js" /> | ||
|
|
||
| </example> | ||
|
|
||
| <references> | ||
|
|
||
| <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">Window.postMessage()</a>.</li> | ||
| <li><a href="https://portswigger.net/web-security/dom-based/web-message-manipulation">Web message manipulation</a>.</li> | ||
| <li><a href="https://labs.detectify.com/2016/12/08/the-pitfalls-of-postmessage/">The pitfalls of postMessage</a>.</li> | ||
|
|
||
| </references> | ||
| </qhelp> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| /** | ||
| * @name Missing origin verification in `postMessage` handler | ||
| * @description Missing origin verification in a `postMessage` handler allows any windows to send arbitrary data to the handler. | ||
| * @kind problem | ||
| * @problem.severity warning | ||
| * @security-severity 5 | ||
| * @precision medium | ||
| * @id js/missing-origin-check | ||
| * @tags correctness | ||
| * security | ||
| * external/cwe/cwe-020 | ||
| */ | ||
|
|
||
| import javascript | ||
|
|
||
| /** A function that handles "message" events. */ | ||
| class PostMessageHandler extends DataFlow::FunctionNode { | ||
| override PostMessageEventHandler astNode; | ||
|
|
||
| /** Gets the parameter that contains the event. */ | ||
| DataFlow::ParameterNode getEventParameter() { | ||
| result = DataFlow::parameterNode(astNode.getEventParameter()) | ||
| } | ||
| } | ||
|
|
||
| /** Gets a reference to the event from a postmessage `handler` */ | ||
| DataFlow::SourceNode event(DataFlow::TypeTracker t, PostMessageHandler handler) { | ||
| t.start() and | ||
| result = handler.getEventParameter() | ||
| or | ||
| exists(DataFlow::TypeTracker t2 | result = event(t2, handler).track(t2, t)) | ||
| } | ||
|
|
||
| /** Gets a reference to the .origin from a postmessage event. */ | ||
| DataFlow::SourceNode origin(DataFlow::TypeTracker t, PostMessageHandler handler) { | ||
| t.start() and | ||
| result = event(DataFlow::TypeTracker::end(), handler).getAPropertyRead("origin") | ||
| or | ||
| result = | ||
| origin(t.continue(), handler) | ||
| .getAMethodCall([ | ||
| "toString", "toLowerCase", "toUpperCase", "toLocaleLowerCase", "toLocaleUpperCase" | ||
| ]) | ||
| or | ||
| exists(DataFlow::TypeTracker t2 | result = origin(t2, handler).track(t2, t)) | ||
| } | ||
|
|
||
| /** Gets a reference to the .source from a postmessage event. */ | ||
| DataFlow::SourceNode source(DataFlow::TypeTracker t, PostMessageHandler handler) { | ||
| t.start() and | ||
| result = event(DataFlow::TypeTracker::end(), handler).getAPropertyRead("source") | ||
| or | ||
| exists(DataFlow::TypeTracker t2 | result = source(t2, handler).track(t2, t)) | ||
| } | ||
|
|
||
| /** Gets a reference to the origin or the source of a postmessage event. */ | ||
| DataFlow::SourceNode sourceOrOrigin(PostMessageHandler handler) { | ||
| result = source(DataFlow::TypeTracker::end(), handler) or | ||
| result = origin(DataFlow::TypeTracker::end(), handler) | ||
| } | ||
|
|
||
| /** Holds if there exists a check of the .origin or .source of the postmessage `handler`. */ | ||
| predicate hasOriginCheck(PostMessageHandler handler) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I expected to see various Did you see any FPs due to not recognizing such checks?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I went through the example results, and there was indeed an FP related to startsWith: https://lgtm.com/query/4833361159564412520/ |
||
| // event.origin === "constant" | ||
| exists(EqualityTest test | sourceOrOrigin(handler).flowsToExpr(test.getAnOperand())) | ||
| or | ||
| // set.includes(event.source) | ||
| exists(InclusionTest test | sourceOrOrigin(handler).flowsTo(test.getContainedNode())) | ||
| or | ||
| // "safeOrigin".startsWith(event.origin) | ||
| exists(StringOps::StartsWith starts | | ||
| origin(DataFlow::TypeTracker::end(), handler).flowsTo(starts.getSubstring()) | ||
| ) | ||
| or | ||
| // "safeOrigin".endsWith(event.origin) | ||
| exists(StringOps::EndsWith ends | | ||
| origin(DataFlow::TypeTracker::end(), handler).flowsTo(ends.getSubstring()) | ||
| ) | ||
| } | ||
|
|
||
| from PostMessageHandler handler | ||
| where not hasOriginCheck(handler) | ||
| select handler.getEventParameter(), "Postmessage handler has no origin check." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| category: newQuery | ||
| --- | ||
| * The `js/missing-origin-check` query has been added. It highlights "message" event handlers that do not check the origin of the event. | ||
| The query previously existed as the experimental `js/missing-postmessageorigin-verification` query. |
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| | tst.js:11:20:11:24 | event | Postmessage handler has no origin check. | | ||
| | tst.js:24:27:24:27 | e | Postmessage handler has no origin check. | | ||
| | tst.js:40:27:40:27 | e | Postmessage handler has no origin check. | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Security/CWE-020/MissingOriginCheck.ql |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| window.onmessage = event => { // OK - good origin check | ||
| let origin = event.origin.toLowerCase(); | ||
|
|
||
| if (origin !== window.location.origin) { | ||
| return; | ||
| } | ||
|
|
||
| eval(event.data); | ||
| } | ||
|
|
||
| window.onmessage = event => { // NOT OK - no origin check | ||
| let origin = event.origin.toLowerCase(); | ||
|
|
||
| console.log(origin); | ||
| eval(event.data); | ||
| } | ||
|
|
||
| window.onmessage = event => { // OK - there is an origin check | ||
| if (event.origin === "https://www.example.com") { | ||
| // do something | ||
| } | ||
| } | ||
|
|
||
| self.onmessage = function(e) { // NOT OK | ||
| Commands[e.data.cmd].apply(null, e.data.args); | ||
| }; | ||
|
|
||
| window.onmessage = event => { // OK - there is an origin check | ||
| if (mySet.includes(event.origin)) { | ||
| // do something | ||
| } | ||
| } | ||
|
|
||
| window.onmessage = event => { // OK - there is an origin check | ||
| if (mySet.includes(event.source)) { | ||
| // do something | ||
| } | ||
| } | ||
|
|
||
| self.onmessage = function(e) { // NOT OK | ||
| Commands[e.data.cmd].apply(null, e.data.args); | ||
| }; | ||
|
|
||
| window.addEventListener('message', function(e) { // OK - has a good origin check | ||
| if (is_sysend_post_message(e) && is_valid_origin(e.origin)) { | ||
| var payload = JSON.parse(e.data); | ||
| if (payload && payload.name === uniq_prefix) { | ||
| var data = unserialize(payload.data); | ||
| sysend.broadcast(payload.key, data); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| function is_valid_origin(origin) { | ||
| if (!domains) { | ||
| warn("no domains configured"); | ||
| return true; | ||
| } | ||
| var valid = domains.includes(origin); | ||
| if (!valid) { | ||
| warn("invalid origin: " + origin); | ||
| } | ||
| return valid; | ||
| } | ||
|
|
||
| window.onmessage = event => { // OK - the check is OK | ||
| if ("https://www.example.com".startsWith(event.origin)) { | ||
| // do something | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not relevant for this PR:
Hmm. I would consider it a bug to use those instead of the canonical
to(Lower|Upper)Casecalls, but perhaps the programmers are saved by the fact that all browser origins normalizes the same way regardless of locales.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would definitely be weird to use the
Localevariants of those methods.I mainly included them because Copilot suggested them.