Skip to content

Commit baa7e35

Browse files
authored
Merge pull request github#18834 from Napalys/js/tanstack
JS: Support 'response' threat model and @tanstack/react-query
2 parents 0522f3f + 3360829 commit baa7e35

File tree

9 files changed

+128
-0
lines changed

9 files changed

+128
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
---
5+
* 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.
6+
* Added support for the `useQuery` hook from `@tanstack/react-query`.

javascript/ql/lib/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ import semmle.javascript.frameworks.Webix
139139
import semmle.javascript.frameworks.WebSocket
140140
import semmle.javascript.frameworks.XmlParsers
141141
import semmle.javascript.frameworks.xUnit
142+
import semmle.javascript.frameworks.Tanstack
142143
import semmle.javascript.linters.ESLint
143144
import semmle.javascript.linters.JSLint
144145
import semmle.javascript.linters.Linting

javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,4 +861,31 @@ module ClientRequest {
861861
result = form.getMember("append").getACall().getParameter(1).asSink()
862862
}
863863
}
864+
865+
/**
866+
* Threat model source representing HTTP response data.
867+
* Marks nodes originating from a client request's response data as tainted.
868+
*/
869+
private class ClientRequestThreatModel extends ThreatModelSource::Range {
870+
ClientRequestThreatModel() { this = any(ClientRequest r).getAResponseDataNode() }
871+
872+
override string getThreatModel() { result = "response" }
873+
874+
override string getSourceType() { result = "HTTP response data" }
875+
}
876+
877+
/**
878+
* An additional taint step that captures taint propagation from the receiver of fetch response methods
879+
* (such as "json", "text", "blob", and "arrayBuffer") to the call result.
880+
*/
881+
private class FetchResponseStep extends TaintTracking::AdditionalTaintStep {
882+
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
883+
exists(DataFlow::MethodCallNode call |
884+
call.getMethodName() in ["json", "text", "blob", "arrayBuffer"] and
885+
node1 = call.getReceiver() and
886+
node2 = call and
887+
call.getNumArgument() = 0
888+
)
889+
}
890+
}
864891
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Provides classes and predicates modeling the Tanstack/react-query library.
3+
*/
4+
5+
private import javascript
6+
7+
/**
8+
* An additional flow step that propagates data from the return value of the query function,
9+
* defined in a useQuery call from the '@tanstack/react-query' module, to the 'data' property.
10+
*/
11+
private class TanstackStep extends DataFlow::AdditionalFlowStep {
12+
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
13+
exists(API::CallNode useQuery |
14+
useQuery = useQueryCall() and
15+
node1 = useQuery.getParameter(0).getMember("queryFn").getReturn().getPromised().asSink() and
16+
node2 = useQuery.getReturn().getMember("data").asSource()
17+
)
18+
}
19+
}
20+
21+
/**
22+
* Retrieves a call node representing a useQuery invocation from the '@tanstack/react-query' module.
23+
*/
24+
private API::CallNode useQueryCall() {
25+
result = API::moduleImport("@tanstack/react-query").getMember("useQuery").getACall()
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#select
2+
| 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 |
3+
edges
4+
| test.jsx:5:11:5:63 | response | test.jsx:6:24:6:31 | response | provenance | |
5+
| test.jsx:5:22:5:63 | await f ... ntent") | test.jsx:5:11:5:63 | response | provenance | |
6+
| test.jsx:5:28:5:63 | fetch(" ... ntent") | test.jsx:5:22:5:63 | await f ... ntent") | provenance | |
7+
| test.jsx:6:11:6:38 | data | test.jsx:7:12:7:15 | data | provenance | |
8+
| test.jsx:6:18:6:38 | await r ... .json() | test.jsx:6:11:6:38 | data | provenance | |
9+
| test.jsx:6:24:6:31 | response | test.jsx:6:24:6:38 | response.json() | provenance | |
10+
| test.jsx:6:24:6:38 | response.json() | test.jsx:6:18:6:38 | await r ... .json() | provenance | |
11+
| test.jsx:7:12:7:15 | data | test.jsx:15:11:17:5 | data | provenance | |
12+
| test.jsx:15:11:17:5 | data | test.jsx:27:29:27:32 | data | provenance | |
13+
nodes
14+
| test.jsx:5:11:5:63 | response | semmle.label | response |
15+
| test.jsx:5:22:5:63 | await f ... ntent") | semmle.label | await f ... ntent") |
16+
| test.jsx:5:28:5:63 | fetch(" ... ntent") | semmle.label | fetch(" ... ntent") |
17+
| test.jsx:6:11:6:38 | data | semmle.label | data |
18+
| test.jsx:6:18:6:38 | await r ... .json() | semmle.label | await r ... .json() |
19+
| test.jsx:6:24:6:31 | response | semmle.label | response |
20+
| test.jsx:6:24:6:38 | response.json() | semmle.label | response.json() |
21+
| test.jsx:7:12:7:15 | data | semmle.label | data |
22+
| test.jsx:15:11:17:5 | data | semmle.label | data |
23+
| test.jsx:27:29:27:32 | data | semmle.label | data |
24+
subpaths
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/threat-models
4+
extensible: threatModelConfiguration
5+
data:
6+
- ["response", true, 0]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: Security/CWE-079/Xss.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import { useQuery } from "./wrapper";
3+
4+
const fetchContent = async () => {
5+
const response = await fetch("https://example.com/content"); // $ Source[js/xss]
6+
const data = await response.json();
7+
return data;
8+
};
9+
10+
const getQueryOptions = () => {
11+
return {queryFn: fetchContent};
12+
}
13+
14+
const ContentWithDangerousHtml = () => {
15+
const { data, error, isLoading } = useQuery(
16+
getQueryOptions()
17+
);
18+
19+
if (isLoading) return <div>Loading...</div>;
20+
if (error) return <div>Error fetching content!</div>;
21+
22+
return (
23+
<div>
24+
<h1>Content with Dangerous HTML</h1>
25+
<div
26+
dangerouslySetInnerHTML={{
27+
__html: data, // $ Alert[js/xss]
28+
}}
29+
/>
30+
</div>
31+
);
32+
};
33+
34+
export default ContentWithDangerousHtml;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
export { useQuery }

0 commit comments

Comments
 (0)