From 6a1cfb6aefb7f8e39457d4bfca573452d2110602 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 3 Jun 2025 15:55:23 +0100 Subject: [PATCH 1/5] feat(js): Add Axios Instance support and add tests --- .../javascript/frameworks/ClientRequests.qll | 64 ++++++++++++++++++- .../ClientRequests/ClientRequests.expected | 9 +++ .../frameworks/ClientRequests/axios.ts | 33 ++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts diff --git a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll index 673bdf2de33f..afd2f4473f55 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll @@ -254,7 +254,7 @@ module ClientRequest { method = "request" and result = this.getOptionArgument(0, "data") or - method = ["post", "put"] and + method = ["post", "put", "patch"] and result = [this.getArgument(1), this.getOptionArgument(2, "data")] or method = ["postForm", "putForm", "patchForm"] and result = this.getArgument(1) @@ -289,6 +289,68 @@ module ClientRequest { } } + class AxiosInstanceRequest extends ClientRequest::Range, API::CallNode { + string method; + API::CallNode instance; + + // Instances of axios, e.g. `axios.create({ ... })` + AxiosInstanceRequest() { + instance = axios().getMember(["create", "createInstance"]).getACall() and + method = [httpMethodName(), "request", "postForm", "putForm", "patchForm", "getUri"] and + this = instance.getReturn().getMember(method).getACall() + } + + private int getOptionsArgIndex() { + (method = "get" or method = "delete" or method = "head") and + result = 0 + or + (method = "post" or method = "put" or method = "patch") and + result = 1 + } + + private DataFlow::Node getOptionArgument(string name) { + result = this.getOptionArgument(this.getOptionsArgIndex(), name) + } + + override DataFlow::Node getUrl() { + result = this.getArgument(0) or + result = this.getOptionArgument(urlPropertyName()) + } + + override DataFlow::Node getHost() { + result = instance.getOptionArgument(0, "baseURL") + } + + override DataFlow::Node getADataNode() { + method = ["post", "put", "patch"] 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"]) + } + + /** Gets the response type from the options passed in. */ + string getResponseType() { + exists(DataFlow::Node option | option = instance.getOptionArgument(0, "responseType") | + option.mayHaveStringValue(result) + ) + or + not exists(this.getOptionArgument("responseType")) and + result = "json" + } + + override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) { + responseType = this.getResponseType() and + promise = true and + result = this + or + responseType = this.getResponseType() and + promise = false and + result = this.getReturn().getPromisedError().getMember("response").asSource() + } + } + /** An expression that is used as a credential in a request. */ private class AuthorizationHeader extends CredentialsNode { AuthorizationHeader() { diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected index f787a7e60603..f84b3c0ca3ed 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected @@ -5,6 +5,8 @@ test_ClientRequest | apollo.js:17:1:17:34 | new Pre ... yurl"}) | | apollo.js:20:1:20:77 | createN ... phql'}) | | apollo.js:23:1:23:31 | new Web ... wsUri}) | +| axios.ts:13:5:14:37 | api\\n ... repo}`) | +| axios.ts:25:5:26:45 | api\\n ... , data) | | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | | puppeteer.ts:6:11:6:42 | page.go ... e.com') | @@ -111,6 +113,7 @@ test_ClientRequest | tst.js:349:5:349:30 | axios.g ... url }) | | tst.js:352:5:352:66 | axiosIn ... text"}) | test_getADataNode +| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:26:41:26:44 | data | | 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'} | | superagent.js:6:5:6:32 | superag ... st(url) | superagent.js:6:39:6:42 | data | @@ -159,6 +162,8 @@ test_getADataNode | 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 +| axios.ts:13:5:14:37 | api\\n ... repo}`) | axios.ts:4:14:4:37 | "https: ... ub.com" | +| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:4:14:4:37 | "https: ... ub.com" | | 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 | | tst.js:91:5:91:34 | got(rel ... host}) | tst.js:91:29:91:32 | host | @@ -173,6 +178,8 @@ test_getUrl | apollo.js:17:1:17:34 | new Pre ... yurl"}) | apollo.js:17:26:17:32 | "myurl" | | apollo.js:20:1:20:77 | createN ... phql'}) | apollo.js:20:30:20:75 | 'https: ... raphql' | | apollo.js:23:1:23:31 | new Web ... wsUri}) | apollo.js:23:25:23:29 | wsUri | +| axios.ts:13:5:14:37 | api\\n ... repo}`) | axios.ts:14:12:14:36 | `/repos ... {repo}` | +| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:26:14:26:38 | `/repos ... {repo}` | | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | axiosTest.js:4:11:7:5 | {\\n ... ,\\n } | | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | axiosTest.js:6:14:6:16 | url | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:12:11:17:5 | {\\n ... }\\n } | @@ -289,6 +296,8 @@ test_getUrl | 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 +| axios.ts:13:5:14:37 | api\\n ... repo}`) | axios.ts:13:5:14:37 | api\\n ... repo}`) | json | true | +| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:25:5:26:45 | api\\n ... , data) | json | true | | 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 | | superagent.js:4:5:4:26 | superag ... ', url) | superagent.js:4:5:4:26 | superag ... ', url) | stream | true | diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts b/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts new file mode 100644 index 000000000000..c23c5824cd8e --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts @@ -0,0 +1,33 @@ +import axios from "axios"; + +let api = axios.create({ + baseURL: "https://api.github.com", + timeout: 1000, + responseType: "json", + headers: { "X-Custom-Header": "foobar" } +}); + +export default api; + +export async function getRepo(owner: string, repo: string) { + api + .get(`/repos/${owner}/${repo}`) + .then((response) => { + console.log("Repository data:", response.data); + return response.data; + }) + .catch((error) => { + console.error("Error fetching user:", error); + }); +} + +export async function updateUser(owner: string, repo: string, data: any) { + api + .patch(`/repos/${owner}/${repo}`, data) + .then((response) => { + console.log("User updated:", response.data); + }) + .catch((error) => { + console.error("Error updating user:", error); + }); +} From 2eb5f10850ca1f131a54c909ea2490f39760b63f Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 3 Jun 2025 15:58:49 +0100 Subject: [PATCH 2/5] feat(js): Add Axios instance support change notes --- .../ql/lib/change-notes/2025-06-03-axios-instance-support.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2025-06-03-axios-instance-support.md diff --git a/javascript/ql/lib/change-notes/2025-06-03-axios-instance-support.md b/javascript/ql/lib/change-notes/2025-06-03-axios-instance-support.md new file mode 100644 index 000000000000..baaf1611b774 --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-06-03-axios-instance-support.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added support for Axios instances in the `axios` module. From 3b64bd48abb755987708d3d232fb80b14df4a20a Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 3 Jun 2025 15:59:32 +0100 Subject: [PATCH 3/5] style(js): Update Formatting --- .../ql/lib/semmle/javascript/frameworks/ClientRequests.qll | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll index afd2f4473f55..7476133b0b5f 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll @@ -317,9 +317,7 @@ module ClientRequest { result = this.getOptionArgument(urlPropertyName()) } - override DataFlow::Node getHost() { - result = instance.getOptionArgument(0, "baseURL") - } + override DataFlow::Node getHost() { result = instance.getOptionArgument(0, "baseURL") } override DataFlow::Node getADataNode() { method = ["post", "put", "patch"] and From 79a72fc15bda4c9a7082970762b240e83388591e Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Tue, 3 Jun 2025 16:37:36 +0100 Subject: [PATCH 4/5] fix(js): Update tests --- .../ClientRequests/ClientRequests.expected | 18 ++++++------- .../frameworks/ClientRequests/axios.ts | 25 +++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected index f84b3c0ca3ed..fbbc8832d72f 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected @@ -5,8 +5,8 @@ test_ClientRequest | apollo.js:17:1:17:34 | new Pre ... yurl"}) | | apollo.js:20:1:20:77 | createN ... phql'}) | | apollo.js:23:1:23:31 | new Web ... wsUri}) | -| axios.ts:13:5:14:37 | api\\n ... repo}`) | -| axios.ts:25:5:26:45 | api\\n ... , data) | +| axios.ts:14:32:14:65 | api.get ... repo}`) | +| axios.ts:25:32:25:73 | api.pat ... , data) | | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | | puppeteer.ts:6:11:6:42 | page.go ... e.com') | @@ -113,7 +113,7 @@ test_ClientRequest | tst.js:349:5:349:30 | axios.g ... url }) | | tst.js:352:5:352:66 | axiosIn ... text"}) | test_getADataNode -| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:26:41:26:44 | data | +| axios.ts:25:32:25:73 | api.pat ... , data) | axios.ts:25:69:25:72 | data | | 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'} | | superagent.js:6:5:6:32 | superag ... st(url) | superagent.js:6:39:6:42 | data | @@ -162,8 +162,8 @@ test_getADataNode | 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 -| axios.ts:13:5:14:37 | api\\n ... repo}`) | axios.ts:4:14:4:37 | "https: ... ub.com" | -| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:4:14:4:37 | "https: ... ub.com" | +| axios.ts:14:32:14:65 | api.get ... repo}`) | axios.ts:4:14:4:37 | "https: ... ub.com" | +| axios.ts:25:32:25:73 | api.pat ... , data) | axios.ts:4:14:4:37 | "https: ... ub.com" | | 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 | | tst.js:91:5:91:34 | got(rel ... host}) | tst.js:91:29:91:32 | host | @@ -178,8 +178,8 @@ test_getUrl | apollo.js:17:1:17:34 | new Pre ... yurl"}) | apollo.js:17:26:17:32 | "myurl" | | apollo.js:20:1:20:77 | createN ... phql'}) | apollo.js:20:30:20:75 | 'https: ... raphql' | | apollo.js:23:1:23:31 | new Web ... wsUri}) | apollo.js:23:25:23:29 | wsUri | -| axios.ts:13:5:14:37 | api\\n ... repo}`) | axios.ts:14:12:14:36 | `/repos ... {repo}` | -| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:26:14:26:38 | `/repos ... {repo}` | +| axios.ts:14:32:14:65 | api.get ... repo}`) | axios.ts:14:40:14:64 | `/repos ... {repo}` | +| axios.ts:25:32:25:73 | api.pat ... , data) | axios.ts:25:42:25:66 | `/repos ... {repo}` | | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | axiosTest.js:4:11:7:5 | {\\n ... ,\\n } | | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | axiosTest.js:6:14:6:16 | url | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:12:11:17:5 | {\\n ... }\\n } | @@ -296,8 +296,8 @@ test_getUrl | 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 -| axios.ts:13:5:14:37 | api\\n ... repo}`) | axios.ts:13:5:14:37 | api\\n ... repo}`) | json | true | -| axios.ts:25:5:26:45 | api\\n ... , data) | axios.ts:25:5:26:45 | api\\n ... , data) | json | true | +| axios.ts:14:32:14:65 | api.get ... repo}`) | axios.ts:14:32:14:65 | api.get ... repo}`) | json | true | +| axios.ts:25:32:25:73 | api.pat ... , data) | axios.ts:25:32:25:73 | api.pat ... , data) | json | true | | 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 | | superagent.js:4:5:4:26 | superag ... ', url) | superagent.js:4:5:4:26 | superag ... ', url) | stream | true | diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts b/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts index c23c5824cd8e..7099e3889b9a 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/axios.ts @@ -10,24 +10,23 @@ let api = axios.create({ export default api; export async function getRepo(owner: string, repo: string) { - api - .get(`/repos/${owner}/${repo}`) - .then((response) => { + try { + const response = await api.get(`/repos/${owner}/${repo}`); console.log("Repository data:", response.data); return response.data; - }) - .catch((error) => { - console.error("Error fetching user:", error); - }); + } catch (error) { + console.error("Error fetching repo:", error); + throw error; + } } export async function updateUser(owner: string, repo: string, data: any) { - api - .patch(`/repos/${owner}/${repo}`, data) - .then((response) => { + try { + const response = await api.patch(`/repos/${owner}/${repo}`, data); console.log("User updated:", response.data); - }) - .catch((error) => { + return response.data; + } catch (error) { console.error("Error updating user:", error); - }); + throw error; + } } From 302097ec85ba0ff70a1d67e9dae49589d841f593 Mon Sep 17 00:00:00 2001 From: GeekMasher Date: Thu, 5 Jun 2025 09:52:25 +0100 Subject: [PATCH 5/5] docs(js): Add AxiosInstanceRequest docs --- .../ql/lib/semmle/javascript/frameworks/ClientRequests.qll | 3 +++ 1 file changed, 3 insertions(+) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll index 7476133b0b5f..22db9f24b99e 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll @@ -289,6 +289,9 @@ module ClientRequest { } } + /** + * A model of a `axios` instance request. + */ class AxiosInstanceRequest extends ClientRequest::Range, API::CallNode { string method; API::CallNode instance;