diff --git a/javascript/ql/lib/change-notes/2025-03-24-axios-additional-methods.md b/javascript/ql/lib/change-notes/2025-03-24-axios-additional-methods.md new file mode 100644 index 000000000000..9f31730bc14b --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-03-24-axios-additional-methods.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +Enhanced `axios` support with new methods (`postForm`, `putForm`, `patchForm`, `getUri`, `create`) and added support for `interceptors.request` and `interceptors.response`. diff --git a/javascript/ql/lib/ext/axios.model.yml b/javascript/ql/lib/ext/axios.model.yml new file mode 100644 index 000000000000..69740c31e299 --- /dev/null +++ b/javascript/ql/lib/ext/axios.model.yml @@ -0,0 +1,12 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: sinkModel + data: + - ["axios", "Member[interceptors].Member[request].Member[use].Argument[0].Parameter[0].Member[url]", "request-forgery"] + + - addsTo: + pack: codeql/javascript-all + extensible: sourceModel + data: + - ["axios", "Member[interceptors].Member[response].Member[use].Argument[0].Parameter[0]", "response"] diff --git a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll index c2b01cf73178..673bdf2de33f 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll @@ -222,7 +222,10 @@ module ClientRequest { method = "request" or this = axios().getMember(method).getACall() and - method = [httpMethodName(), "request"] + method = [httpMethodName(), "request", "postForm", "putForm", "patchForm", "getUri"] + or + this = axios().getMember("create").getReturn().getACall() and + method = "request" } private int getOptionsArgIndex() { @@ -254,6 +257,8 @@ module ClientRequest { method = ["post", "put"] and result = [this.getArgument(1), this.getOptionArgument(2, "data")] or + method = ["postForm", "putForm", "patchForm"] and result = this.getArgument(1) + or result = this.getOptionArgument([0 .. 2], ["headers", "params"]) } diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected index bb3a73004536..f787a7e60603 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected @@ -103,6 +103,13 @@ test_ClientRequest | tst.js:334:5:334:25 | got.pag ... rl, {}) | | tst.js:337:5:337:20 | jsonClient.get() | | tst.js:340:5:340:21 | jsonClient2.get() | +| tst.js:344:5:344:37 | axios.p ... config) | +| tst.js:345:5:345:28 | axios.p ... , data) | +| tst.js:346:5:346:36 | axios.p ... config) | +| tst.js:347:5:347:30 | axios.p ... , data) | +| tst.js:348:5:348:38 | axios.p ... config) | +| tst.js:349:5:349:30 | axios.g ... url }) | +| tst.js:352:5:352:66 | axiosIn ... text"}) | test_getADataNode | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:15:18:15:55 | { 'Cont ... json' } | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:16:15:16:35 | {x: 'te ... 'test'} | @@ -146,6 +153,11 @@ test_getADataNode | tst.js:257:1:262:2 | form.su ... rs()\\n}) | tst.js:255:25:255:35 | 'new_value' | | tst.js:286:20:286:55 | new Web ... :8080') | tst.js:288:21:288:35 | 'Hello Server!' | | tst.js:321:5:321:32 | superag ... st(url) | tst.js:321:39:321:42 | data | +| tst.js:344:5:344:37 | axios.p ... config) | tst.js:344:25:344:28 | data | +| tst.js:345:5:345:28 | axios.p ... , data) | tst.js:345:24:345:27 | data | +| tst.js:346:5:346:36 | axios.p ... config) | tst.js:346:24:346:27 | data | +| tst.js:347:5:347:30 | axios.p ... , data) | tst.js:347:26:347:29 | data | +| tst.js:348:5:348:38 | axios.p ... config) | tst.js:348:26:348:29 | data | test_getHost | tst.js:87:5:87:39 | http.ge ... host}) | tst.js:87:34:87:37 | host | | tst.js:89:5:89:23 | axios({host: host}) | tst.js:89:18:89:21 | host | @@ -268,6 +280,14 @@ test_getUrl | tst.js:337:5:337:20 | jsonClient.get() | tst.js:336:41:336:43 | url | | tst.js:340:5:340:21 | jsonClient2.get() | tst.js:339:42:339:44 | url | | tst.js:340:5:340:21 | jsonClient2.get() | tst.js:339:61:339:63 | url | +| tst.js:344:5:344:37 | axios.p ... config) | tst.js:344:20:344:22 | url | +| tst.js:345:5:345:28 | axios.p ... , data) | tst.js:345:19:345:21 | url | +| tst.js:346:5:346:36 | axios.p ... config) | tst.js:346:19:346:21 | url | +| tst.js:347:5:347:30 | axios.p ... , data) | tst.js:347:21:347:23 | url | +| tst.js:348:5:348:38 | axios.p ... config) | tst.js:348:21:348:23 | url | +| tst.js:349:5:349:30 | axios.g ... url }) | tst.js:349:18:349:29 | { url: url } | +| tst.js:352:5:352:66 | axiosIn ... text"}) | tst.js:352:19:352:65 | {method ... "text"} | +| tst.js:352:5:352:66 | axiosIn ... text"}) | tst.js:352:40:352:42 | url | test_getAResponseDataNode | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | json | true | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | json | true | @@ -354,3 +374,10 @@ test_getAResponseDataNode | tst.js:334:5:334:25 | got.pag ... rl, {}) | tst.js:334:5:334:25 | got.pag ... rl, {}) | text | true | | tst.js:337:5:337:20 | jsonClient.get() | tst.js:337:5:337:20 | jsonClient.get() | text | true | | tst.js:340:5:340:21 | jsonClient2.get() | tst.js:340:5:340:21 | jsonClient2.get() | text | true | +| tst.js:344:5:344:37 | axios.p ... config) | tst.js:344:5:344:37 | axios.p ... config) | json | true | +| tst.js:345:5:345:28 | axios.p ... , data) | tst.js:345:5:345:28 | axios.p ... , data) | json | true | +| tst.js:346:5:346:36 | axios.p ... config) | tst.js:346:5:346:36 | axios.p ... config) | json | true | +| tst.js:347:5:347:30 | axios.p ... , data) | tst.js:347:5:347:30 | axios.p ... , data) | json | true | +| tst.js:348:5:348:38 | axios.p ... config) | tst.js:348:5:348:38 | axios.p ... config) | json | true | +| tst.js:349:5:349:30 | axios.g ... url }) | tst.js:349:5:349:30 | axios.g ... url }) | json | true | +| tst.js:352:5:352:66 | axiosIn ... text"}) | tst.js:352:5:352:66 | axiosIn ... text"}) | text | true | diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js index c9fc40dc5068..3cd086fae0e7 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js @@ -339,3 +339,15 @@ function gotTests(url){ const jsonClient2 = got.extend({url: url}).extend({url: url}); jsonClient2.get(); } + +function moreAxiosTests(url, data, config){ + axios.postForm(url, data, config); + axios.putForm(url, data); + axios.putForm(url, data, config); + axios.patchForm(url, data); + axios.patchForm(url, data, config); + axios.getUri({ url: url }); + + const axiosInstance = axios.create({}); + axiosInstance({method: "get", url: url, responseType: "text"}); +} 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 ef0c88098b98..afc30e246085 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,4 +1,5 @@ #select +| interceptors.js:9:56:9:72 | userGeneratedHtml | interceptors.js:7:6:7:13 | response | interceptors.js:9:56:9:72 | userGeneratedHtml | Cross-site scripting vulnerability due to $@. | interceptors.js:7:6:7:13 | response | user-provided value | | 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 | @@ -18,6 +19,9 @@ | 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 | | testUseQueries.vue:25:10:25:23 | v-html=data2 | testUseQueries.vue:11:36:11:49 | fetch("${id}") | testUseQueries.vue:25:10:25:23 | v-html=data2 | Cross-site scripting vulnerability due to $@. | testUseQueries.vue:11:36:11:49 | fetch("${id}") | user-provided value | edges +| interceptors.js:7:6:7:13 | response | interceptors.js:8:35:8:42 | response | provenance | | +| interceptors.js:8:15:8:47 | userGeneratedHtml | interceptors.js:9:56:9:72 | userGeneratedHtml | provenance | | +| interceptors.js:8:35:8:42 | response | interceptors.js:8:15:8:47 | userGeneratedHtml | provenance | | | 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 | | @@ -96,6 +100,10 @@ edges | testUseQueries.vue:12:20:12:34 | response.json() | testUseQueries.vue:18:22:18:36 | results[0].data | provenance | | | testUseQueries.vue:18:22:18:36 | results[0].data | testUseQueries.vue:25:10:25:23 | v-html=data2 | provenance | | nodes +| interceptors.js:7:6:7:13 | response | semmle.label | response | +| interceptors.js:8:15:8:47 | userGeneratedHtml | semmle.label | userGeneratedHtml | +| interceptors.js:8:35:8:42 | response | semmle.label | response | +| interceptors.js:9:56:9:72 | userGeneratedHtml | semmle.label | userGeneratedHtml | | 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") | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/interceptors.js b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/interceptors.js new file mode 100644 index 000000000000..d4e918c1de8d --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXssWithResponseThreat/interceptors.js @@ -0,0 +1,20 @@ +const express = require("express"); +const axios = require("axios"); + +const app = express(); + +axios.interceptors.response.use( + (response) => { // $ Source + const userGeneratedHtml = response.data; + document.getElementById("content").innerHTML = userGeneratedHtml; // $ Alert + return response; + }, + (error) => { + return Promise.reject(error); + } +); + +app.post("/fetch", (req, res) => { + const { url } = req.body; + axios.get(url); +}); diff --git a/javascript/ql/test/query-tests/Security/CWE-918/RequestForgery.expected b/javascript/ql/test/query-tests/Security/CWE-918/RequestForgery.expected index a8ed8b29b4be..6b33506d502f 100644 --- a/javascript/ql/test/query-tests/Security/CWE-918/RequestForgery.expected +++ b/javascript/ql/test/query-tests/Security/CWE-918/RequestForgery.expected @@ -1,5 +1,6 @@ #select | apollo.serverSide.ts:8:39:8:64 | get(fil ... => {}) | apollo.serverSide.ts:7:36:7:44 | { files } | apollo.serverSide.ts:8:43:8:50 | file.url | The $@ of this request depends on a $@. | apollo.serverSide.ts:8:43:8:50 | file.url | URL | apollo.serverSide.ts:7:36:7:44 | { files } | user-provided value | +| axiosInterceptors.serverSide.js:11:26:11:40 | userProvidedUrl | axiosInterceptors.serverSide.js:19:21:19:28 | req.body | axiosInterceptors.serverSide.js:11:26:11:40 | userProvidedUrl | The $@ of this request depends on a $@. | axiosInterceptors.serverSide.js:11:26:11:40 | userProvidedUrl | endpoint | axiosInterceptors.serverSide.js:19:21:19:28 | req.body | user-provided value | | serverSide.js:18:5:18:20 | request(tainted) | serverSide.js:14:29:14:35 | req.url | serverSide.js:18:13:18:19 | tainted | The $@ of this request depends on a $@. | serverSide.js:18:13:18:19 | tainted | URL | serverSide.js:14:29:14:35 | req.url | user-provided value | | serverSide.js:20:5:20:24 | request.get(tainted) | serverSide.js:14:29:14:35 | req.url | serverSide.js:20:17:20:23 | tainted | The $@ of this request depends on a $@. | serverSide.js:20:17:20:23 | tainted | URL | serverSide.js:14:29:14:35 | req.url | user-provided value | | serverSide.js:24:5:24:20 | request(options) | serverSide.js:14:29:14:35 | req.url | serverSide.js:23:19:23:25 | tainted | The $@ of this request depends on a $@. | serverSide.js:23:19:23:25 | tainted | URL | serverSide.js:14:29:14:35 | req.url | user-provided value | @@ -30,6 +31,11 @@ edges | apollo.serverSide.ts:8:13:8:17 | files | apollo.serverSide.ts:8:28:8:31 | file | provenance | | | apollo.serverSide.ts:8:28:8:31 | file | apollo.serverSide.ts:8:43:8:46 | file | provenance | | | apollo.serverSide.ts:8:43:8:46 | file | apollo.serverSide.ts:8:43:8:50 | file.url | provenance | | +| axiosInterceptors.serverSide.js:19:11:19:17 | { url } | axiosInterceptors.serverSide.js:19:11:19:28 | url | provenance | | +| axiosInterceptors.serverSide.js:19:11:19:28 | url | axiosInterceptors.serverSide.js:20:23:20:25 | url | provenance | | +| axiosInterceptors.serverSide.js:19:21:19:28 | req.body | axiosInterceptors.serverSide.js:19:11:19:17 | { url } | provenance | | +| axiosInterceptors.serverSide.js:20:5:20:25 | userProvidedUrl | axiosInterceptors.serverSide.js:11:26:11:40 | userProvidedUrl | provenance | | +| axiosInterceptors.serverSide.js:20:23:20:25 | url | axiosInterceptors.serverSide.js:20:5:20:25 | userProvidedUrl | provenance | | | serverSide.js:14:9:14:52 | tainted | serverSide.js:18:13:18:19 | tainted | provenance | | | serverSide.js:14:9:14:52 | tainted | serverSide.js:20:17:20:23 | tainted | provenance | | | serverSide.js:14:9:14:52 | tainted | serverSide.js:23:19:23:25 | tainted | provenance | | @@ -85,6 +91,12 @@ nodes | apollo.serverSide.ts:8:28:8:31 | file | semmle.label | file | | apollo.serverSide.ts:8:43:8:46 | file | semmle.label | file | | apollo.serverSide.ts:8:43:8:50 | file.url | semmle.label | file.url | +| axiosInterceptors.serverSide.js:11:26:11:40 | userProvidedUrl | semmle.label | userProvidedUrl | +| axiosInterceptors.serverSide.js:19:11:19:17 | { url } | semmle.label | { url } | +| axiosInterceptors.serverSide.js:19:11:19:28 | url | semmle.label | url | +| axiosInterceptors.serverSide.js:19:21:19:28 | req.body | semmle.label | req.body | +| axiosInterceptors.serverSide.js:20:5:20:25 | userProvidedUrl | semmle.label | userProvidedUrl | +| axiosInterceptors.serverSide.js:20:23:20:25 | url | semmle.label | url | | serverSide.js:14:9:14:52 | tainted | semmle.label | tainted | | serverSide.js:14:19:14:42 | url.par ... , true) | semmle.label | url.par ... , true) | | serverSide.js:14:29:14:35 | req.url | semmle.label | req.url | diff --git a/javascript/ql/test/query-tests/Security/CWE-918/axiosInterceptors.serverSide.js b/javascript/ql/test/query-tests/Security/CWE-918/axiosInterceptors.serverSide.js new file mode 100644 index 000000000000..12e95a326cbc --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-918/axiosInterceptors.serverSide.js @@ -0,0 +1,22 @@ +const express = require("express"); +const axios = require("axios"); + +const app = express(); + +let userProvidedUrl = ""; + +axios.interceptors.request.use( + function (config) { + if (userProvidedUrl) { + config.url = userProvidedUrl; // $ Alert[js/request-forgery] + } + return config; + }, + error => error +); + +app.post("/fetch", (req, res) => { + const { url } = req.body; // $ Source[js/request-forgery] + userProvidedUrl = url; + axios.get("placeholder"); +});