diff --git a/javascript/ql/lib/change-notes/2025-02-21-tanstack.md b/javascript/ql/lib/change-notes/2025-02-21-tanstack.md new file mode 100644 index 000000000000..49169ddc943f --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-02-21-tanstack.md @@ -0,0 +1,6 @@ +--- +category: majorAnalysis +--- +--- +* Added support for the `response` threat model kind, which can enabled with [advanced setup](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#extending-codeql-coverage-with-threat-models). When enabled, the response data coming back from an outgoing HTTP request is considered a source of taint. +* Added support for the `useQuery` hook from `@tanstack/react-query`. diff --git a/javascript/ql/lib/javascript.qll b/javascript/ql/lib/javascript.qll index 7bb2b7676105..b3bf7399a621 100644 --- a/javascript/ql/lib/javascript.qll +++ b/javascript/ql/lib/javascript.qll @@ -139,6 +139,7 @@ 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/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll index 13879fda3e7d..720d917d4985 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll @@ -861,4 +861,31 @@ module ClientRequest { result = form.getMember("append").getACall().getParameter(1).asSink() } } + + /** + * Threat model source representing HTTP response data. + * Marks nodes originating from a client request's response data as tainted. + */ + private class ClientRequestThreatModel extends ThreatModelSource::Range { + ClientRequestThreatModel() { this = any(ClientRequest r).getAResponseDataNode() } + + override string getThreatModel() { result = "response" } + + override string getSourceType() { result = "HTTP response data" } + } + + /** + * 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. + */ + private class FetchResponseStep extends TaintTracking::AdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + exists(DataFlow::MethodCallNode call | + call.getMethodName() in ["json", "text", "blob", "arrayBuffer"] and + node1 = call.getReceiver() and + node2 = call and + call.getNumArgument() = 0 + ) + } + } } diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll b/javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll new file mode 100644 index 000000000000..741079575963 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll @@ -0,0 +1,26 @@ +/** + * 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 new file mode 100644 index 000000000000..f83c9f40b315 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.expected @@ -0,0 +1,24 @@ +#select +| 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 | +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 | | +| test.jsx:5:28:5:63 | fetch(" ... ntent") | test.jsx:5:22:5:63 | await f ... ntent") | provenance | | +| test.jsx:6:11:6:38 | data | test.jsx:7:12:7:15 | data | provenance | | +| 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:15:11:17:5 | data | test.jsx:27:29:27:32 | data | 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") | +| test.jsx:5:28:5:63 | fetch(" ... ntent") | semmle.label | fetch(" ... ntent") | +| test.jsx:6:11:6:38 | data | semmle.label | data | +| test.jsx:6:18:6:38 | await r ... .json() | semmle.label | await r ... .json() | +| test.jsx:6:24:6:31 | response | semmle.label | response | +| 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:27:29:27:32 | data | semmle.label | data | +subpaths diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.ext.yml b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.ext.yml new file mode 100644 index 000000000000..e364808a3497 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.ext.yml @@ -0,0 +1,6 @@ +extensions: + - addsTo: + pack: codeql/threat-models + extensible: threatModelConfiguration + data: + - ["response", true, 0] diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.qlref b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.qlref new file mode 100644 index 000000000000..22add04d21de --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/Xss.qlref @@ -0,0 +1,2 @@ +query: Security/CWE-079/Xss.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.jsx b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.jsx new file mode 100644 index 000000000000..26d24167b424 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import { useQuery } from "./wrapper"; + +const fetchContent = async () => { + const response = await fetch("https://example.com/content"); // $ Source[js/xss] + const data = await response.json(); + return data; +}; + +const getQueryOptions = () => { + return {queryFn: fetchContent}; +} + +const ContentWithDangerousHtml = () => { + const { data, error, isLoading } = useQuery( + getQueryOptions() + ); + + if (isLoading) return
Loading...
; + if (error) return
Error fetching content!
; + + return ( +
+

Content with Dangerous HTML

+
+
+ ); +}; + +export default ContentWithDangerousHtml; diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/wrapper.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/wrapper.js new file mode 100644 index 000000000000..07326c0f1c59 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/wrapper.js @@ -0,0 +1,2 @@ +import { useQuery } from "@tanstack/react-query"; +export { useQuery }