Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions javascript/ql/lib/change-notes/2025-02-21-tanstack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
category: minorAnalysis
category: majorAnalysis

---
* Added support for the `useQuery` hook from `@tanstack/react-query`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Added support for the `useQuery` hook from `@tanstack/react-query`.
* 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`.

1 change: 1 addition & 0 deletions javascript/ql/lib/javascript.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
}
26 changes: 26 additions & 0 deletions javascript/ql/lib/semmle/javascript/frameworks/Tanstack.qll
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extensions:
- addsTo:
pack: codeql/threat-models
extensible: threatModelConfiguration
data:
- ["response", true, 0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: Security/CWE-079/Xss.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
Original file line number Diff line number Diff line change
@@ -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 <div>Loading...</div>;
if (error) return <div>Error fetching content!</div>;

return (
<div>
<h1>Content with Dangerous HTML</h1>
<div
dangerouslySetInnerHTML={{
__html: data, // $ Alert[js/xss]
}}
/>
</div>
);
};

export default ContentWithDangerousHtml;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { useQuery } from "@tanstack/react-query";
export { useQuery }