Merge pull request #21368 from asgerf/browser-sources

JS: Add 'browser' source kinds
This commit is contained in:
Asger F
2026-03-16 09:24:54 +01:00
committed by GitHub
10 changed files with 87 additions and 18 deletions

View File

@@ -406,7 +406,7 @@ Adds a new taint source. Most taint-tracking queries will use the new source.
- **type**: Name of a type from which to evaluate **path**.
- **path**: Access path leading to the source.
- **kind**: Kind of source to add. Currently only **remote** is used.
- **kind**: Kind of source to add. See the section on source kinds for a list of supported kinds.
Example:
@@ -553,7 +553,16 @@ Kinds
Source kinds
~~~~~~~~~~~~
See documentation below for :ref:`Threat models <threat-models-javascript>`.
- **remote**: A general source of remote flow.
- **browser**: A source in the browser environment that does not fit a more specific browser kind.
- **browser-url-query**: A source derived from the query parameters of the browser URL, such as ``location.search``.
- **browser-url-fragment**: A source derived from the fragment part of the browser URL, such as ``location.hash``.
- **browser-url-path**: A source derived from the pathname of the browser URL, such as ``location.pathname``.
- **browser-url**: A source derived from the browser URL, where the untrusted part is prefixed by trusted data such as the scheme and hostname.
- **browser-window-name**: A source derived from the window name, such as ``window.name``.
- **browser-message-event**: A source derived from cross-window message passing, such as ``event`` in ``window.onmessage = event => {...}``.
See also :ref:`Threat models <threat-models-javascript>`.
Sink kinds
~~~~~~~~~~

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added support for browser-specific source kinds (`browser`, `browser-url-query`, `browser-url-fragment`, `browser-url-path`, `browser-url`, `browser-window-name`, `browser-message-event`) that can be used in data extensions to model sources in browser environments.

View File

@@ -35,6 +35,18 @@ private class RemoteFlowSourceFromMaD extends RemoteFlowSource {
override string getSourceType() { result = "Remote flow" }
}
private class ClientSideRemoteFlowSourceFromMaD extends ClientSideRemoteFlowSource {
private ClientSideRemoteFlowKind kind;
ClientSideRemoteFlowSourceFromMaD() { ModelOutput::sourceNode(this, kind) }
override ClientSideRemoteFlowKind getKind() { result = kind }
override string getSourceType() {
result = "Source node (" + this.getThreatModel() + ") [from data-extension]"
}
}
/**
* A threat-model flow source originating from a data extension.
*/

View File

@@ -43,53 +43,65 @@ import Cached
/**
* A type of remote flow source that is specific to the browser environment.
*
* The underlying string also corresponds to a source kind.
*/
class ClientSideRemoteFlowKind extends string {
ClientSideRemoteFlowKind() {
this = ["query", "fragment", "path", "url", "name", "message-event"]
this =
[
"browser", "browser-url-query", "browser-url-fragment", "browser-url-path", "browser-url",
"browser-window-name", "browser-message-event"
]
}
/**
* Holds if this is the `query` kind, describing sources derived from the query parameters of the browser URL,
* Holds if this is the `browser` kind, indicating a remote source in a browser context, that does not fit into one
* of the more specific kinds.
*/
predicate isGenericBrowserSourceKind() { this = "browser" }
/**
* Holds if this is the `browser-url-query` kind, describing sources derived from the query parameters of the browser URL,
* such as `location.search`.
*/
predicate isQuery() { this = "query" }
predicate isQuery() { this = "browser-url-query" }
/**
* Holds if this is the `frgament` kind, describing sources derived from the fragment part of the browser URL,
* Holds if this is the `browser-url-fragment` kind, describing sources derived from the fragment part of the browser URL,
* such as `location.hash`.
*/
predicate isFragment() { this = "fragment" }
predicate isFragment() { this = "browser-url-fragment" }
/**
* Holds if this is the `path` kind, describing sources derived from the pathname of the browser URL,
* Holds if this is the `browser-url-path` kind, describing sources derived from the pathname of the browser URL,
* such as `location.pathname`.
*/
predicate isPath() { this = "path" }
predicate isPath() { this = "browser-url-path" }
/**
* Holds if this is the `url` kind, describing sources derived from the browser URL,
* Holds if this is the `browser-url` kind, describing sources derived from the browser URL,
* where the untrusted part of the URL is prefixed by trusted data, such as the scheme and hostname.
*/
predicate isUrl() { this = "url" }
predicate isUrl() { this = "browser-url" }
/** Holds if this is the `query` or `fragment` kind. */
/** Holds if this is the `browser-url-query` or `browser-url-fragment` kind. */
predicate isQueryOrFragment() { this.isQuery() or this.isFragment() }
/** Holds if this is the `path`, `query`, or `fragment` kind. */
/** Holds if this is the `browser-url-path`, `browser-url-query`, or `browser-url-fragment` kind. */
predicate isPathOrQueryOrFragment() { this.isPath() or this.isQuery() or this.isFragment() }
/** Holds if this is the `path` or `url` kind. */
/** Holds if this is the `browser-url-path` or `browser-url` kind. */
predicate isPathOrUrl() { this.isPath() or this.isUrl() }
/** Holds if this is the `name` kind, describing sources derived from the window name, such as `window.name`. */
predicate isWindowName() { this = "name" }
/** Holds if this is the `browser-window-name` kind, describing sources derived from the window name, such as `window.name`. */
predicate isWindowName() { this = "browser-window-name" }
/**
* Holds if this is the `message-event` kind, describing sources derived from cross-window message passing,
* Holds if this is the `browser-message-event` kind, describing sources derived from cross-window message passing,
* such as `event` in `window.onmessage = event => {...}`.
*/
predicate isMessageEvent() { this = "message-event" }
predicate isMessageEvent() { this = "browser-message-event" }
}
/**

View File

@@ -3,6 +3,7 @@
| clientSide.js:14:5:14:64 | request ... search) | clientSide.js:14:42:14:63 | window. ... .search | clientSide.js:14:13:14:63 | 'https: ... .search | The $@ of this request depends on a $@. | clientSide.js:14:13:14:63 | 'https: ... .search | URL | clientSide.js:14:42:14:63 | window. ... .search | user-provided value |
| clientSide.js:17:5:17:58 | request ... '/id') | clientSide.js:16:22:16:41 | window.location.hash | clientSide.js:17:13:17:57 | 'https: ... + '/id' | The $@ of this request depends on a $@. | clientSide.js:17:13:17:57 | 'https: ... + '/id' | URL | clientSide.js:16:22:16:41 | window.location.hash | user-provided value |
| clientSide.js:21:5:21:54 | request ... '/id') | clientSide.js:20:18:20:28 | window.name | clientSide.js:21:13:21:53 | 'https: ... + '/id' | The $@ of this request depends on a $@. | clientSide.js:21:13:21:53 | 'https: ... + '/id' | URL | clientSide.js:20:18:20:28 | window.name | user-provided value |
| clientSide.js:27:5:27:19 | request(custom) | clientSide.js:26:20:26:56 | require ... ource() | clientSide.js:27:13:27:18 | custom | The $@ of this request depends on a $@. | clientSide.js:27:13:27:18 | custom | URL | clientSide.js:26:20:26:56 | require ... ource() | user-provided value |
edges
| clientSide.js:11:11:11:15 | query | clientSide.js:12:42:12:46 | query | provenance | |
| clientSide.js:11:19:11:40 | window. ... .search | clientSide.js:11:19:11:53 | window. ... ring(1) | provenance | |
@@ -16,6 +17,8 @@ edges
| clientSide.js:20:11:20:14 | name | clientSide.js:21:42:21:45 | name | provenance | |
| clientSide.js:20:18:20:28 | window.name | clientSide.js:20:11:20:14 | name | provenance | |
| clientSide.js:21:42:21:45 | name | clientSide.js:21:13:21:53 | 'https: ... + '/id' | provenance | |
| clientSide.js:26:11:26:16 | custom | clientSide.js:27:13:27:18 | custom | provenance | |
| clientSide.js:26:20:26:56 | require ... ource() | clientSide.js:26:11:26:16 | custom | provenance | |
nodes
| clientSide.js:11:11:11:15 | query | semmle.label | query |
| clientSide.js:11:19:11:40 | window. ... .search | semmle.label | window. ... .search |
@@ -33,4 +36,7 @@ nodes
| clientSide.js:20:18:20:28 | window.name | semmle.label | window.name |
| clientSide.js:21:13:21:53 | 'https: ... + '/id' | semmle.label | 'https: ... + '/id' |
| clientSide.js:21:42:21:45 | name | semmle.label | name |
| clientSide.js:26:11:26:16 | custom | semmle.label | custom |
| clientSide.js:26:20:26:56 | require ... ource() | semmle.label | require ... ource() |
| clientSide.js:27:13:27:18 | custom | semmle.label | custom |
subpaths

View File

@@ -0,0 +1,7 @@
extensions:
- addsTo:
pack: codeql/javascript-all
extensible: sourceModel
data:
- ['testlib', 'Member[getBrowserSource].ReturnValue', 'browser-url-query']
- ['testlib', 'Member[getServerSource].ReturnValue', 'remote']

View File

@@ -39,6 +39,7 @@
| serverSide.js:143:5:143:26 | axios.g ... t.href) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:143:15:143:25 | target.href | The $@ of this request depends on a $@. | serverSide.js:143:15:143:25 | target.href | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value |
| serverSide.js:145:5:145:25 | axios.g ... dedUrl) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:145:15:145:24 | encodedUrl | The $@ of this request depends on a $@. | serverSide.js:145:15:145:24 | encodedUrl | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value |
| serverSide.js:147:5:147:25 | axios.g ... pedUrl) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:147:15:147:24 | escapedUrl | The $@ of this request depends on a $@. | serverSide.js:147:15:147:24 | escapedUrl | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value |
| serverSide.js:151:1:151:15 | request(custom) | serverSide.js:150:16:150:51 | require ... ource() | serverSide.js:151:9:151:14 | custom | The $@ of this request depends on a $@. | serverSide.js:151:9:151:14 | custom | URL | serverSide.js:150:16:150:51 | require ... ource() | user-provided value |
edges
| Request/app/api/proxy/route2.serverSide.ts:4:9:4:15 | { url } | Request/app/api/proxy/route2.serverSide.ts:4:11:4:13 | url | provenance | |
| Request/app/api/proxy/route2.serverSide.ts:4:11:4:13 | url | Request/app/api/proxy/route2.serverSide.ts:5:27:5:29 | url | provenance | |
@@ -144,6 +145,8 @@ edges
| serverSide.js:146:11:146:20 | escapedUrl | serverSide.js:147:15:147:24 | escapedUrl | provenance | |
| serverSide.js:146:24:146:36 | escape(input) | serverSide.js:146:11:146:20 | escapedUrl | provenance | |
| serverSide.js:146:31:146:35 | input | serverSide.js:146:24:146:36 | escape(input) | provenance | |
| serverSide.js:150:7:150:12 | custom | serverSide.js:151:9:151:14 | custom | provenance | |
| serverSide.js:150:16:150:51 | require ... ource() | serverSide.js:150:7:150:12 | custom | provenance | |
nodes
| Request/app/api/proxy/route2.serverSide.ts:4:9:4:15 | { url } | semmle.label | { url } |
| Request/app/api/proxy/route2.serverSide.ts:4:11:4:13 | url | semmle.label | url |
@@ -271,4 +274,7 @@ nodes
| serverSide.js:146:24:146:36 | escape(input) | semmle.label | escape(input) |
| serverSide.js:146:31:146:35 | input | semmle.label | input |
| serverSide.js:147:15:147:24 | escapedUrl | semmle.label | escapedUrl |
| serverSide.js:150:7:150:12 | custom | semmle.label | custom |
| serverSide.js:150:16:150:51 | require ... ource() | semmle.label | require ... ource() |
| serverSide.js:151:9:151:14 | custom | semmle.label | custom |
subpaths

View File

@@ -0,0 +1,7 @@
extensions:
- addsTo:
pack: codeql/javascript-all
extensible: sourceModel
data:
- ['testlib', 'Member[getBrowserSource].ReturnValue', 'browser-url-query']
- ['testlib', 'Member[getServerSource].ReturnValue', 'remote']

View File

@@ -22,4 +22,7 @@ export function MyComponent() {
request('https://example.com/api?q=' + name);
request(window.location.href + '?q=123');
const custom = require('testlib').getBrowserSource(); // $ Source[js/client-side-request-forgery]
request(custom); // $ Alert[js/client-side-request-forgery]
}

View File

@@ -146,3 +146,6 @@ var server2 = http.createServer(function (req, res) {
const escapedUrl = escape(input);
axios.get(escapedUrl); // $ Alert[js/request-forgery]
});
const custom = require('testlib').getServerSource(); // $ Source[js/request-forgery]
request(custom); // $ Alert[js/request-forgery]