diff --git a/javascript/ql/lib/change-notes/2025-03-11-tanstack-angular.md b/javascript/ql/lib/change-notes/2025-03-11-tanstack-angular.md new file mode 100644 index 000000000000..5c4eb99eed76 --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-03-11-tanstack-angular.md @@ -0,0 +1,5 @@ +--- +category: minorAnalysis +--- +* Added support for the `@tanstack/angular-query-experimental` package. +* Improved support for the `@angular/common/http` package, detecting outgoing HTTP requests in more cases. diff --git a/javascript/ql/lib/ext/tanstack.model.yml b/javascript/ql/lib/ext/tanstack.model.yml new file mode 100644 index 000000000000..19b57eae6124 --- /dev/null +++ b/javascript/ql/lib/ext/tanstack.model.yml @@ -0,0 +1,7 @@ +extensions: + - addsTo: + 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"] diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll b/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll index a2702dbc7429..3441b5d9c006 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll @@ -190,13 +190,16 @@ module Angular2 { result.hasUnderlyingType("@angular/common/http", "HttpClient") } + /** Gets a reference to an `HttpClient` object using the API graph. */ + API::Node httpClientApiNode() { result = API::Node::ofType("@angular/common/http", "HttpClient") } + private class AngularClientRequest extends ClientRequest::Range, DataFlow::MethodCallNode { int argumentOffset; AngularClientRequest() { - this = httpClient().getAMethodCall("request") and argumentOffset = 1 + this = httpClientApiNode().getMember("request").getACall() and argumentOffset = 1 or - this = httpClient().getAMethodCall() and + this = httpClientApiNode().getAMember().getACall() and not this.getMethodName() = "request" and argumentOffset = 0 } 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 b2be2a7bcc2e..313febe21946 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,7 @@ #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 | +| 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 | | 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 | @@ -20,6 +22,15 @@ edges | 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 | | +| 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 | | +| test.ts:20:28:20:35 | response | test.ts:24:67:24:74 | response | provenance | | +| test.ts:21:57:21:64 | response | test.ts:21:57:21:76 | response.description | provenance | | +| test.ts:24:43:24:50 | response | test.ts:24:43:24:55 | response.name | provenance | | +| 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 | | | 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 +67,15 @@ nodes | 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 | +| test.ts:8:9:8:79 | this.#h ... query') | semmle.label | this.#h ... query') | +| test.ts:20:28:20:35 | response | semmle.label | response | +| test.ts:21:57:21:64 | response | semmle.label | response | +| test.ts:21:57:21:76 | response.description | semmle.label | response.description | +| test.ts:24:36:24:90 | `

${ ... o}

` | semmle.label | `

${ ... o}

` | +| test.ts:24:43:24:50 | response | semmle.label | response | +| 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 | | 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 | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.ts b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.ts new file mode 100644 index 000000000000..fb7ffae102ad --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/test.ts @@ -0,0 +1,37 @@ +import { QueryClient, injectQuery } from '@tanstack/angular-query-experimental' +import { HttpClient } from '@angular/common/http' + +class ServiceOrComponent { + query = injectQuery(() => ({ + queryKey: ['repoData'], + queryFn: () => + this.#http.get('https://api.github.com/repos/tanstack/query'), // $ Source + })) + + #http: { + get: (url: string) => Promise + }; + + constructor(http: HttpClient) { + this.#http = http; + } + + displayRepoDetails() { + this.query.data.then(response => { + document.getElementById('repoInfo').innerHTML = response.description; // $ Alert + + const detailsElement = document.createElement('div'); + detailsElement.innerHTML = `

${response.name}

${response.owner.bio}

`; // $ Alert + document.body.appendChild(detailsElement); + }); + } +} + +interface Response { + name: string; + description: string; + stargazers_count: number; + owner: { + bio: string; + } +}