From cbdc74e2500a1d030e56244e632d971c91eac3c1 Mon Sep 17 00:00:00 2001
From: Keshav Malik
Date: Sat, 1 Aug 2026 20:26:29 +0530
Subject: [PATCH] JS: Track response data through promises
---
...2026-08-01-client-response-promise-data.md | 4 +
.../javascript/frameworks/ClientRequests.qll | 12 +
.../Xss.expected | 250 ++++++++++++++++++
.../fetch-promise-chain.js | 145 ++++++++++
.../non-fetch-promise-chain.js | 47 ++++
5 files changed, 458 insertions(+)
create mode 100644 javascript/ql/lib/change-notes/2026-08-01-client-response-promise-data.md
create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/fetch-promise-chain.js
create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/non-fetch-promise-chain.js
diff --git a/javascript/ql/lib/change-notes/2026-08-01-client-response-promise-data.md b/javascript/ql/lib/change-notes/2026-08-01-client-response-promise-data.md
new file mode 100644
index 000000000000..7213d64a830d
--- /dev/null
+++ b/javascript/ql/lib/change-notes/2026-08-01-client-response-promise-data.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* JavaScript security queries using the `response` threat model now track promise-wrapped client response data into promise fulfillment values. This may improve results for queries such as `js/xss` when response data is consumed through `.then(...)` chains.
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll
index 9da93400ef92..fabf0a6d38f5 100644
--- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll
+++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll
@@ -1016,6 +1016,18 @@ module ClientRequest {
override string getSourceType() { result = "HTTP response data" }
}
+ /**
+ * A taint step from promise-wrapped response data to the value that the promise resolves to.
+ */
+ private class ClientRequestResponsePromiseStep extends TaintTracking::SharedTaintStep {
+ override predicate promiseStep(DataFlow::Node node1, DataFlow::Node node2) {
+ exists(ClientRequest r |
+ r.getAResponseDataNode(_, true).getALocalSource().flowsTo(node1) and
+ PromiseFlow::loadStep(node1, node2, Promises::valueProp())
+ )
+ }
+ }
+
/**
* An additional taint step that captures taint propagation from the receiver of fetch response methods
* (such as "json", "text", "blob", and "arrayBuffer") to the call result.
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected
index 99cafddc41d2..8d4eae0a7083 100644
--- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected
@@ -1,5 +1,19 @@
#select
+| fetch-promise-chain.js:21:47:21:49 | row | fetch-promise-chain.js:7:3:7:12 | fetch(url) | fetch-promise-chain.js:21:47:21:49 | row | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:7:3:7:12 | fetch(url) | user-provided value |
+| fetch-promise-chain.js:30:33:30:36 | text | fetch-promise-chain.js:27:3:27:23 | fetch(" ... ssage") | fetch-promise-chain.js:30:33:30:36 | text | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:27:3:27:23 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:47:33:47:44 | data.message | fetch-promise-chain.js:43:19:43:39 | fetch(" ... ssage") | fetch-promise-chain.js:47:33:47:44 | data.message | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:43:19:43:39 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:57:33:57:36 | text | fetch-promise-chain.js:53:13:53:33 | fetch(" ... ssage") | fetch-promise-chain.js:57:33:57:36 | text | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:53:13:53:33 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:67:55:67:66 | item.message | fetch-promise-chain.js:62:3:62:23 | fetch(" ... ssage") | fetch-promise-chain.js:67:55:67:66 | item.message | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:62:3:62:23 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:76:33:76:39 | message | fetch-promise-chain.js:73:3:73:23 | fetch(" ... ssage") | fetch-promise-chain.js:76:33:76:39 | message | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:73:3:73:23 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:84:33:84:44 | data.message | fetch-promise-chain.js:81:3:81:23 | fetch(" ... ssage") | fetch-promise-chain.js:84:33:84:44 | data.message | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:81:3:81:23 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:93:31:93:42 | data.message | fetch-promise-chain.js:90:5:90:25 | fetch(" ... ssage") | fetch-promise-chain.js:93:31:93:42 | data.message | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:90:5:90:25 | fetch(" ... ssage") | user-provided value |
+| fetch-promise-chain.js:102:33:102:47 | payload.message | fetch-promise-chain.js:98:3:98:23 | fetch(" ... ssage") | fetch-promise-chain.js:102:33:102:47 | payload.message | Cross-site scripting vulnerability due to $@. | fetch-promise-chain.js:98:3:98:23 | fetch(" ... ssage") | user-provided value |
| interceptors.js:9:56:9:72 | userGeneratedHtml | interceptors.js:7:6:7:13 | response | interceptors.js:9:56:9:72 | userGeneratedHtml | Cross-site scripting vulnerability due to $@. | interceptors.js:7:6:7:13 | response | user-provided value |
+| non-fetch-promise-chain.js:5:33:5:36 | body | non-fetch-promise-chain.js:3:3:3:35 | rp("htt ... ssage") | non-fetch-promise-chain.js:5:33:5:36 | body | Cross-site scripting vulnerability due to $@. | non-fetch-promise-chain.js:3:3:3:35 | rp("htt ... ssage") | user-provided value |
+| non-fetch-promise-chain.js:13:33:13:44 | data.message | non-fetch-promise-chain.js:11:3:11:56 | rp({ ur ... true }) | non-fetch-promise-chain.js:13:33:13:44 | data.message | Cross-site scripting vulnerability due to $@. | non-fetch-promise-chain.js:11:3:11:56 | rp({ ur ... true }) | user-provided value |
+| non-fetch-promise-chain.js:21:31:21:51 | respons ... message | non-fetch-promise-chain.js:19:19:19:58 | axios.g ... ssage") | non-fetch-promise-chain.js:21:31:21:51 | respons ... message | Cross-site scripting vulnerability due to $@. | non-fetch-promise-chain.js:19:19:19:58 | axios.g ... ssage") | user-provided value |
+| non-fetch-promise-chain.js:29:33:29:53 | respons ... message | non-fetch-promise-chain.js:27:3:27:46 | needle( ... ssage") | non-fetch-promise-chain.js:29:33:29:53 | respons ... message | Cross-site scripting vulnerability due to $@. | non-fetch-promise-chain.js:27:3:27:46 | needle( ... ssage") | user-provided value |
+| non-fetch-promise-chain.js:37:33:37:45 | response.text | non-fetch-promise-chain.js:35:3:35:47 | superag ... ssage") | non-fetch-promise-chain.js:37:33:37:45 | response.text | Cross-site scripting vulnerability due to $@. | non-fetch-promise-chain.js:35:3:35:47 | superag ... ssage") | user-provided value |
| test.jsx:27:29:27:32 | data | test.jsx:5:28:5:63 | fetch(" ... ntent") | test.jsx:27:29:27:32 | data | Cross-site scripting vulnerability due to $@. | test.jsx:5:28:5:63 | fetch(" ... ntent") | user-provided value |
| test.ts:21:57:21:76 | response.description | test.ts:8:9:8:79 | this.#h ... query') | test.ts:21:57:21:76 | response.description | Cross-site scripting vulnerability due to $@. | test.ts:8:9:8:79 | this.#h ... query') | user-provided value |
| test.ts:24:36:24:90 | `${ ... o}
` | test.ts:8:9:8:79 | this.#h ... query') | test.ts:24:36:24:90 | `