diff --git a/javascript/ql/lib/change-notes/2025-03-13-tanstack-vue.md b/javascript/ql/lib/change-notes/2025-03-13-tanstack-vue.md
new file mode 100644
index 000000000000..defc6c78bc2a
--- /dev/null
+++ b/javascript/ql/lib/change-notes/2025-03-13-tanstack-vue.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* Added support for the `@tanstack/vue-query` package.
diff --git a/javascript/ql/lib/ext/tanstack.model.yml b/javascript/ql/lib/ext/tanstack.model.yml
index 19b57eae6124..d2b0bc0d782f 100644
--- a/javascript/ql/lib/ext/tanstack.model.yml
+++ b/javascript/ql/lib/ext/tanstack.model.yml
@@ -3,5 +3,9 @@ extensions:
pack: codeql/javascript-all
extensible: summaryModel
data:
- - ["@tanstack/angular-query-experimental", "Member[injectQuery]", "Argument[0].ReturnValue.Member[queryFn].ReturnValue", "ReturnValue.Member[data].Awaited", "taint"]
- - ["@tanstack/angular-query", "Member[injectQuery]", "Argument[0].ReturnValue.Member[queryFn].ReturnValue", "ReturnValue.Member[data].Awaited", "taint"]
+ - ["@tanstack/angular-query-experimental", "Member[injectQuery]", "Argument[0].ReturnValue.Member[queryFn].ReturnValue", "ReturnValue.Member[data].Awaited", "value"]
+ - ["@tanstack/angular-query", "Member[injectQuery]", "Argument[0].ReturnValue.Member[queryFn].ReturnValue", "ReturnValue.Member[data].Awaited", "value"]
+ - ["@tanstack/vue-query", "Member[useQuery]", "Argument[0].Member[queryFn].ReturnValue.Awaited", "ReturnValue.Member[data]", "value"]
+ - ["@tanstack/vue-query", "Member[useQueries]", "Argument[0].Member[queries].ArrayElement.Member[queryFn].ReturnValue.Awaited", "ReturnValue.AnyMember.Member[data]", "value"]
+ - ["@tanstack/react-query", "Member[useQueries]", "Argument[0].Member[queries].ArrayElement.Member[queryFn].ReturnValue.Awaited", "ReturnValue.AnyMember.Member[data]", "value"]
+ - ["@tanstack/react-query", "Member[useQuery]", "Argument[0].Member[queryFn].ReturnValue.Awaited", "ReturnValue.Member[data]", "value"]
diff --git a/javascript/ql/lib/javascript.qll b/javascript/ql/lib/javascript.qll
index b3bf7399a621..7bb2b7676105 100644
--- a/javascript/ql/lib/javascript.qll
+++ b/javascript/ql/lib/javascript.qll
@@ -139,7 +139,6 @@ import semmle.javascript.frameworks.Webix
import semmle.javascript.frameworks.WebSocket
import semmle.javascript.frameworks.XmlParsers
import semmle.javascript.frameworks.xUnit
-import semmle.javascript.frameworks.Tanstack
import semmle.javascript.linters.ESLint
import semmle.javascript.linters.JSLint
import semmle.javascript.linters.Linting
diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll b/javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll
deleted file mode 100644
index 741079575963..000000000000
--- a/javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Provides classes and predicates modeling the Tanstack/react-query library.
- */
-
-private import javascript
-
-/**
- * An additional flow step that propagates data from the return value of the query function,
- * defined in a useQuery call from the '@tanstack/react-query' module, to the 'data' property.
- */
-private class TanstackStep extends DataFlow::AdditionalFlowStep {
- override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
- exists(API::CallNode useQuery |
- useQuery = useQueryCall() and
- node1 = useQuery.getParameter(0).getMember("queryFn").getReturn().getPromised().asSink() and
- node2 = useQuery.getReturn().getMember("data").asSource()
- )
- }
-}
-
-/**
- * Retrieves a call node representing a useQuery invocation from the '@tanstack/react-query' module.
- */
-private API::CallNode useQueryCall() {
- result = API::moduleImport("@tanstack/react-query").getMember("useQuery").getACall()
-}
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 313febe21946..7aa3957d64d8 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
@@ -2,6 +2,7 @@
| 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 | `${ ... o}` | Cross-site scripting vulnerability due to $@. | test.ts:8:9:8:79 | this.#h ... query') | user-provided value |
+| test.vue:22:10:22:22 | v-html=data | test.vue:10:32:10:84 | fetch(" ... sts/1") | test.vue:22:10:22:22 | v-html=data | Cross-site scripting vulnerability due to $@. | test.vue:10:32:10:84 | fetch(" ... sts/1") | user-provided value |
| testReactRelay.tsx:7:43:7:58 | commentData.text | testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | testReactRelay.tsx:7:43:7:58 | commentData.text | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | user-provided value |
| testReactRelay.tsx:18:48:18:68 | data.co ... 0].text | testReactRelay.tsx:17:16:17:42 | useLazy ... ry, {}) | testReactRelay.tsx:18:48:18:68 | data.co ... 0].text | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:17:16:17:42 | useLazy ... ry, {}) | user-provided value |
| testReactRelay.tsx:28:17:28:67 | usePrel ... r?.name | testReactRelay.tsx:28:17:28:56 | usePrel ... erence) | testReactRelay.tsx:28:17:28:67 | usePrel ... r?.name | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:28:17:28:56 | usePrel ... erence) | user-provided value |
@@ -12,6 +13,9 @@
| testReactRelay.tsx:113:48:113:58 | fragmentRef | testReactRelay.tsx:100:14:100:16 | res | testReactRelay.tsx:113:48:113:58 | fragmentRef | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:100:14:100:16 | res | user-provided value |
| testReactRelay.tsx:127:35:127:43 | data.user | testReactRelay.tsx:124:12:124:15 | data | testReactRelay.tsx:127:35:127:43 | data.user | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:124:12:124:15 | data | user-provided value |
| testReactRelay.tsx:137:50:137:53 | data | testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | testReactRelay.tsx:137:50:137:53 | data | Cross-site scripting vulnerability due to $@. | testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | user-provided value |
+| testReactUseQueries.jsx:37:25:37:38 | repoQuery.data | testReactUseQueries.jsx:4:26:4:53 | fetch(' ... e.com') | testReactUseQueries.jsx:37:25:37:38 | repoQuery.data | Cross-site scripting vulnerability due to $@. | testReactUseQueries.jsx:4:26:4:53 | fetch(' ... e.com') | user-provided value |
+| testUseQueries2.vue:40:10:40:23 | v-html=data3 | testUseQueries2.vue:6:28:6:63 | fetch(" ... ntent") | testUseQueries2.vue:40:10:40:23 | v-html=data3 | Cross-site scripting vulnerability due to $@. | testUseQueries2.vue:6:28:6:63 | fetch(" ... ntent") | user-provided value |
+| testUseQueries2.vue:40:10:40:23 | v-html=data3 | testUseQueries2.vue:12:28:12:41 | fetch("${id}") | testUseQueries2.vue:40:10:40:23 | v-html=data3 | Cross-site scripting vulnerability due to $@. | testUseQueries2.vue:12:28:12:41 | fetch("${id}") | user-provided value |
edges
| test.jsx:5:11:5:63 | response | test.jsx:6:24:6:31 | response | provenance | |
| test.jsx:5:22:5:63 | await f ... ntent") | test.jsx:5:11:5:63 | response | provenance | |
@@ -20,8 +24,9 @@ edges
| test.jsx:6:18:6:38 | await r ... .json() | test.jsx:6:11:6:38 | data | provenance | |
| test.jsx:6:24:6:31 | response | test.jsx:6:24:6:38 | response.json() | provenance | |
| test.jsx:6:24:6:38 | response.json() | test.jsx:6:18:6:38 | await r ... .json() | provenance | |
-| test.jsx:7:12:7:15 | data | test.jsx:15:11:17:5 | data | provenance | |
+| test.jsx:7:12:7:15 | data | test.jsx:15:13:15:16 | data | provenance | |
| test.jsx:15:11:17:5 | data | test.jsx:27:29:27:32 | data | provenance | |
+| test.jsx:15:13:15:16 | data | test.jsx:15:11:17:5 | data | provenance | |
| test.ts:8:9:8:79 | this.#h ... query') | test.ts:20:28:20:35 | response | provenance | |
| test.ts:20:28:20:35 | response | test.ts:21:57:21:64 | response | provenance | |
| test.ts:20:28:20:35 | response | test.ts:24:43:24:50 | response | provenance | |
@@ -31,6 +36,14 @@ edges
| test.ts:24:43:24:55 | response.name | test.ts:24:36:24:90 | `${ ... o}` | provenance | |
| test.ts:24:67:24:74 | response | test.ts:24:67:24:84 | response.owner.bio | provenance | |
| test.ts:24:67:24:84 | response.owner.bio | test.ts:24:36:24:90 | `${ ... o}` | provenance | |
+| test.vue:7:11:13:6 | data | test.vue:15:21:15:24 | data | provenance | |
+| test.vue:7:45:7:48 | data | test.vue:7:11:13:6 | data | provenance | |
+| test.vue:10:15:10:84 | response | test.vue:11:16:11:23 | response | provenance | |
+| test.vue:10:26:10:84 | await f ... sts/1") | test.vue:10:15:10:84 | response | provenance | |
+| test.vue:10:32:10:84 | fetch(" ... sts/1") | test.vue:10:26:10:84 | await f ... sts/1") | provenance | |
+| test.vue:11:16:11:23 | response | test.vue:11:16:11:30 | response.json() | provenance | |
+| test.vue:11:16:11:30 | response.json() | test.vue:7:45:7:48 | data | provenance | |
+| test.vue:15:21:15:24 | data | test.vue:22:10:22:22 | v-html=data | provenance | |
| testReactRelay.tsx:5:9:5:52 | commentData | testReactRelay.tsx:7:43:7:53 | commentData | provenance | |
| testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | testReactRelay.tsx:5:9:5:52 | commentData | provenance | |
| testReactRelay.tsx:7:43:7:53 | commentData | testReactRelay.tsx:7:43:7:58 | commentData.text | provenance | |
@@ -56,6 +69,25 @@ edges
| testReactRelay.tsx:127:35:127:38 | data | testReactRelay.tsx:127:35:127:43 | data.user | provenance | |
| testReactRelay.tsx:136:9:136:39 | data | testReactRelay.tsx:137:50:137:53 | data | provenance | |
| testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | testReactRelay.tsx:136:9:136:39 | data | provenance | |
+| testReactUseQueries.jsx:4:9:4:53 | response | testReactUseQueries.jsx:5:10:5:17 | response | provenance | |
+| testReactUseQueries.jsx:4:20:4:53 | await f ... e.com') | testReactUseQueries.jsx:4:9:4:53 | response | provenance | |
+| testReactUseQueries.jsx:4:26:4:53 | fetch(' ... e.com') | testReactUseQueries.jsx:4:20:4:53 | await f ... e.com') | provenance | |
+| testReactUseQueries.jsx:5:10:5:17 | response | testReactUseQueries.jsx:5:10:5:24 | response.json() | provenance | |
+| testReactUseQueries.jsx:5:10:5:24 | response.json() | testReactUseQueries.jsx:37:25:37:38 | repoQuery.data | provenance | |
+| testUseQueries2.vue:6:11:6:63 | response | testUseQueries2.vue:7:24:7:31 | response | provenance | |
+| testUseQueries2.vue:6:22:6:63 | await f ... ntent") | testUseQueries2.vue:6:11:6:63 | response | provenance | |
+| testUseQueries2.vue:6:28:6:63 | fetch(" ... ntent") | testUseQueries2.vue:6:22:6:63 | await f ... ntent") | provenance | |
+| testUseQueries2.vue:7:11:7:38 | data | testUseQueries2.vue:8:12:8:15 | data | provenance | |
+| testUseQueries2.vue:7:18:7:38 | await r ... .json() | testUseQueries2.vue:7:11:7:38 | data | provenance | |
+| testUseQueries2.vue:7:24:7:31 | response | testUseQueries2.vue:7:24:7:38 | response.json() | provenance | |
+| testUseQueries2.vue:7:24:7:38 | response.json() | testUseQueries2.vue:7:18:7:38 | await r ... .json() | provenance | |
+| testUseQueries2.vue:8:12:8:15 | data | testUseQueries2.vue:33:22:33:36 | results[0].data | provenance | |
+| testUseQueries2.vue:12:11:12:41 | response | testUseQueries2.vue:13:12:13:19 | response | provenance | |
+| testUseQueries2.vue:12:22:12:41 | await fetch("${id}") | testUseQueries2.vue:12:11:12:41 | response | provenance | |
+| testUseQueries2.vue:12:28:12:41 | fetch("${id}") | testUseQueries2.vue:12:22:12:41 | await fetch("${id}") | provenance | |
+| testUseQueries2.vue:13:12:13:19 | response | testUseQueries2.vue:13:12:13:26 | response.json() | provenance | |
+| testUseQueries2.vue:13:12:13:26 | response.json() | testUseQueries2.vue:33:22:33:36 | results[0].data | provenance | |
+| testUseQueries2.vue:33:22:33:36 | results[0].data | testUseQueries2.vue:40:10:40:23 | v-html=data3 | provenance | |
nodes
| test.jsx:5:11:5:63 | response | semmle.label | response |
| test.jsx:5:22:5:63 | await f ... ntent") | semmle.label | await f ... ntent") |
@@ -66,6 +98,7 @@ nodes
| test.jsx:6:24:6:38 | response.json() | semmle.label | response.json() |
| test.jsx:7:12:7:15 | data | semmle.label | data |
| test.jsx:15:11:17:5 | data | semmle.label | data |
+| test.jsx:15:13:15:16 | data | semmle.label | data |
| test.jsx:27:29:27:32 | data | semmle.label | data |
| test.ts:8:9:8:79 | this.#h ... query') | semmle.label | this.#h ... query') |
| test.ts:20:28:20:35 | response | semmle.label | response |
@@ -76,6 +109,15 @@ nodes
| test.ts:24:43:24:55 | response.name | semmle.label | response.name |
| test.ts:24:67:24:74 | response | semmle.label | response |
| test.ts:24:67:24:84 | response.owner.bio | semmle.label | response.owner.bio |
+| test.vue:7:11:13:6 | data | semmle.label | data |
+| test.vue:7:45:7:48 | data | semmle.label | data |
+| test.vue:10:15:10:84 | response | semmle.label | response |
+| test.vue:10:26:10:84 | await f ... sts/1") | semmle.label | await f ... sts/1") |
+| test.vue:10:32:10:84 | fetch(" ... sts/1") | semmle.label | fetch(" ... sts/1") |
+| test.vue:11:16:11:23 | response | semmle.label | response |
+| test.vue:11:16:11:30 | response.json() | semmle.label | response.json() |
+| test.vue:15:21:15:24 | data | semmle.label | data |
+| test.vue:22:10:22:22 | v-html=data | semmle.label | v-html=data |
| testReactRelay.tsx:5:9:5:52 | commentData | semmle.label | commentData |
| testReactRelay.tsx:5:23:5:52 | useFrag ... entRef) | semmle.label | useFrag ... entRef) |
| testReactRelay.tsx:7:43:7:53 | commentData | semmle.label | commentData |
@@ -111,4 +153,25 @@ nodes
| testReactRelay.tsx:136:9:136:39 | data | semmle.label | data |
| testReactRelay.tsx:136:16:136:39 | readFra ... y, key) | semmle.label | readFra ... y, key) |
| testReactRelay.tsx:137:50:137:53 | data | semmle.label | data |
+| testReactUseQueries.jsx:4:9:4:53 | response | semmle.label | response |
+| testReactUseQueries.jsx:4:20:4:53 | await f ... e.com') | semmle.label | await f ... e.com') |
+| testReactUseQueries.jsx:4:26:4:53 | fetch(' ... e.com') | semmle.label | fetch(' ... e.com') |
+| testReactUseQueries.jsx:5:10:5:17 | response | semmle.label | response |
+| testReactUseQueries.jsx:5:10:5:24 | response.json() | semmle.label | response.json() |
+| testReactUseQueries.jsx:37:25:37:38 | repoQuery.data | semmle.label | repoQuery.data |
+| testUseQueries2.vue:6:11:6:63 | response | semmle.label | response |
+| testUseQueries2.vue:6:22:6:63 | await f ... ntent") | semmle.label | await f ... ntent") |
+| testUseQueries2.vue:6:28:6:63 | fetch(" ... ntent") | semmle.label | fetch(" ... ntent") |
+| testUseQueries2.vue:7:11:7:38 | data | semmle.label | data |
+| testUseQueries2.vue:7:18:7:38 | await r ... .json() | semmle.label | await r ... .json() |
+| testUseQueries2.vue:7:24:7:31 | response | semmle.label | response |
+| testUseQueries2.vue:7:24:7:38 | response.json() | semmle.label | response.json() |
+| testUseQueries2.vue:8:12:8:15 | data | semmle.label | data |
+| testUseQueries2.vue:12:11:12:41 | response | semmle.label | response |
+| testUseQueries2.vue:12:22:12:41 | await fetch("${id}") | semmle.label | await fetch("${id}") |
+| testUseQueries2.vue:12:28:12:41 | fetch("${id}") | semmle.label | fetch("${id}") |
+| testUseQueries2.vue:13:12:13:19 | response | semmle.label | response |
+| testUseQueries2.vue:13:12:13:26 | response.json() | semmle.label | response.json() |
+| testUseQueries2.vue:33:22:33:36 | results[0].data | semmle.label | results[0].data |
+| testUseQueries2.vue:40:10:40:23 | v-html=data3 | semmle.label | v-html=data3 |
subpaths
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.vue b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.vue
new file mode 100644
index 000000000000..0db6637a8cb5
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testReactUseQueries.jsx b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testReactUseQueries.jsx
new file mode 100644
index 000000000000..d978e309c0f1
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testReactUseQueries.jsx
@@ -0,0 +1,42 @@
+import { useQueries } from '@tanstack/react-query';
+
+const fetchRepoData = async () => {
+ const response = await fetch('https://example.com'); // $ Source
+ return response.json();
+};
+
+async function fetchPost() {
+ const response = await fetch("www.example.com"); // $ MISSING: Source
+ return response.json();
+}
+
+export default function UseQueriesComponent() {
+ const results = useQueries({
+ queries: [
+ {
+ queryKey: ['repoData'],
+ queryFn: fetchRepoData,
+ },
+ {
+ queryKey: ['repoData'],
+ queryFn: () => fetchPost,
+ },
+ ],
+ });
+
+ const repoQuery = results[0];
+
+ if (repoQuery.isLoading) return
Loading...
;
+ if (repoQuery.isError) return Error: {repoQuery.error.message}
;
+
+ return (
+
+
Content with Dangerous HTML
+
+
+);
+}
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testUseQueries.vue b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testUseQueries.vue
new file mode 100644
index 000000000000..c02c0a79023f
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testUseQueries.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testUseQueries2.vue b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testUseQueries2.vue
new file mode 100644
index 000000000000..8515e2d33ff8
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/testUseQueries2.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+