From f1a3293f4c81bb870add0eb4ea083dc34f6e2aa6 Mon Sep 17 00:00:00 2001 From: Napalys Date: Mon, 14 Apr 2025 11:04:56 +0200 Subject: [PATCH 1/7] Added change note --- javascript/ql/lib/change-notes/2025-04-14-fastify-addhook.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2025-04-14-fastify-addhook.md diff --git a/javascript/ql/lib/change-notes/2025-04-14-fastify-addhook.md b/javascript/ql/lib/change-notes/2025-04-14-fastify-addhook.md new file mode 100644 index 000000000000..a9e754bd56ea --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-04-14-fastify-addhook.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added support for the `fastify` `addHook` method. From c1750816982a41ed13caf1e65ecc7912198220b5 Mon Sep 17 00:00:00 2001 From: Napalys Date: Tue, 15 Apr 2025 09:33:41 +0200 Subject: [PATCH 2/7] Added test cases for `fastify.addHook` --- .../CodeInjection/CodeInjection.expected | 13 +++ .../HeuristicSourceCodeInjection.expected | 9 ++ .../Security/CWE-094/CodeInjection/fastify.js | 103 ++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected index a81b9dbcce0f..5fc5856814c7 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected @@ -27,6 +27,10 @@ | express.js:20:34:20:38 | taint | express.js:19:17:19:35 | req.param("wobble") | express.js:20:34:20:38 | taint | This code execution depends on a $@. | express.js:19:17:19:35 | req.param("wobble") | user-provided value | | express.js:36:15:36:19 | taint | express.js:27:17:27:35 | req.param("wobble") | express.js:36:15:36:19 | taint | This code execution depends on a $@. | express.js:27:17:27:35 | req.param("wobble") | user-provided value | | express.js:43:10:43:12 | msg | express.js:42:30:42:32 | msg | express.js:43:10:43:12 | msg | This code execution depends on a $@. | express.js:42:30:42:32 | msg | user-provided value | +| fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value | +| fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | +| fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value | +| fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | This code execution depends on a $@. | module.js:9:16:9:29 | req.query.code | user-provided value | | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | This code execution depends on a $@. | module.js:11:17:11:30 | req.query.code | user-provided value | | react-native.js:8:32:8:38 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:8:32:8:38 | tainted | This code execution depends on a $@. | react-native.js:7:17:7:33 | req.param("code") | user-provided value | @@ -75,6 +79,10 @@ edges | express.js:27:9:27:35 | taint | express.js:36:15:36:19 | taint | provenance | | | express.js:27:17:27:35 | req.param("wobble") | express.js:27:9:27:35 | taint | provenance | | | express.js:42:30:42:32 | msg | express.js:43:10:43:12 | msg | provenance | | +| fastify.js:57:9:57:39 | userInput | fastify.js:58:44:58:52 | userInput | provenance | | +| fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | | +| fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | | +| fastify.js:57:21:57:39 | request.query.input | fastify.js:57:9:57:39 | userInput | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted | provenance | | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | provenance | | @@ -144,6 +152,11 @@ nodes | express.js:36:15:36:19 | taint | semmle.label | taint | | express.js:42:30:42:32 | msg | semmle.label | msg | | express.js:43:10:43:12 | msg | semmle.label | msg | +| fastify.js:57:9:57:39 | userInput | semmle.label | userInput | +| fastify.js:57:21:57:33 | request.query | semmle.label | request.query | +| fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | +| fastify.js:58:44:58:52 | userInput | semmle.label | userInput | +| fastify.js:59:23:59:31 | userInput | semmle.label | userInput | | module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code | | module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code | | react-native.js:7:7:7:33 | tainted | semmle.label | tainted | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected index ba973943e124..b56f28a79171 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected @@ -11,6 +11,10 @@ edges | express.js:27:9:27:35 | taint | express.js:36:15:36:19 | taint | provenance | | | express.js:27:17:27:35 | req.param("wobble") | express.js:27:9:27:35 | taint | provenance | | | express.js:42:30:42:32 | msg | express.js:43:10:43:12 | msg | provenance | | +| fastify.js:57:9:57:39 | userInput | fastify.js:58:44:58:52 | userInput | provenance | | +| fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | | +| fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | | +| fastify.js:57:21:57:39 | request.query.input | fastify.js:57:9:57:39 | userInput | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted | provenance | | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | provenance | | @@ -82,6 +86,11 @@ nodes | express.js:36:15:36:19 | taint | semmle.label | taint | | express.js:42:30:42:32 | msg | semmle.label | msg | | express.js:43:10:43:12 | msg | semmle.label | msg | +| fastify.js:57:9:57:39 | userInput | semmle.label | userInput | +| fastify.js:57:21:57:33 | request.query | semmle.label | request.query | +| fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | +| fastify.js:58:44:58:52 | userInput | semmle.label | userInput | +| fastify.js:59:23:59:31 | userInput | semmle.label | userInput | | module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code | | module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code | | react-native.js:7:7:7:33 | tainted | semmle.label | tainted | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js new file mode 100644 index 000000000000..b8878cdab41c --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js @@ -0,0 +1,103 @@ +const fastify = require('fastify')({ logger: true }); + +fastify.addHook('onRequest', async (request, reply) => { + const userInput = request.query.onRequest; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.addHook('onSend', async (request, reply, payload) => { + const userInput = request.query.onSend; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + return JSON.stringify({ ...JSON.parse(payload), onSend: request.evalResult }); +}); + +fastify.addHook('preParsing', async (request, reply, payload) => { + const userInput = request.query.preParsing; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + return payload; +}); + +fastify.addHook('preValidation', async (request, reply) => { + const userInput = request.query.preValidation; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.addHook('preHandler', async (request, reply) => { + const userInput = request.query.preHandler; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.addHook('preSerialization', async (request, reply, payload) => { + const userInput = request.query.preSerialization; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + return payload; +}); + +fastify.addHook('onResponse', async (request, reply) => { + const userInput = request.query.onResponse; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.addHook('onError', async (request, reply, error) => { + const userInput = request.query.onError; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.addHook('onTimeout', async (request, reply) => { + const userInput = request.query.onTimeout; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.addHook('onRequestAbort', (request, done) => { + const userInput = request.query.onRequestAbort; // $ MISSING: Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] +}); + +fastify.get('/dangerous', async (request, reply) => { + const userInput = request.query.input; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] + const result = eval(userInput); // $ Alert[js/code-injection] + return { result }; +}); + + +// Store user input in request object +fastify.addHook('preHandler', async (request, reply) => { + request.storedCode = request.query.storedCode; +}); +fastify.get('/flow-through-request', async (request, reply) => { + // Use the stored code from previous hook + if (request.storedCode) { + const evaluatedResult = eval(request.storedCode); // $ MISSING: Alert[js/code-injection] + return { result: evaluatedResult }; + } + return { result: null }; +}); + +// Store user input in reply object +fastify.addHook('onRequest', async (request, reply) => { + reply.userCode = request.query.replyCode; +}); +fastify.get('/flow-through-reply', async (request, reply) => { + // Use the code stored in reply object + if (reply.userCode) { + const replyResult = eval(reply.userCode); // $ MISSING: Alert[js/code-injection] + return { result: replyResult }; + } + return { result: null }; +}); + + +// Store user input in reply object +fastify.addHook('onRequest', async (request, reply) => { + reply.locals = reply.locals || {}; + reply.locals.nestedCode = request.query.replyCode; +}); +fastify.get('/flow-through-reply', async (request, reply) => { + // Use the code stored in reply object + if (reply.locals && reply.locals.nestedCode) { + const replyResult = eval(reply.locals.nestedCode); // $ MISSING: Alert[js/code-injection] + return { result: replyResult }; + } + return { result: null }; +}); From 9b194ea613c653a7bf6e16ca4633827b8e8d2a41 Mon Sep 17 00:00:00 2001 From: Napalys Date: Tue, 15 Apr 2025 09:37:13 +0200 Subject: [PATCH 3/7] Added `addHook` to `RouteSetup` thus now it is recognized now as rouute handler --- .../semmle/javascript/frameworks/Fastify.qll | 2 +- .../CodeInjection/CodeInjection.expected | 90 +++++++++++++++++++ .../HeuristicSourceCodeInjection.expected | 70 +++++++++++++++ .../Security/CWE-094/CodeInjection/fastify.js | 40 ++++----- 4 files changed, 181 insertions(+), 21 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll index 2b8d6287d78d..5b2485c76794 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll @@ -138,7 +138,7 @@ module Fastify { RouteSetup() { this = server(server).getAMethodCall(methodName) and - methodName = ["route", "get", "head", "post", "put", "delete", "options", "patch"] + methodName = ["route", "get", "head", "post", "put", "delete", "options", "patch", "addHook"] } override DataFlow::SourceNode getARouteHandler() { diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected index 5fc5856814c7..34487a43e630 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected @@ -27,6 +27,26 @@ | express.js:20:34:20:38 | taint | express.js:19:17:19:35 | req.param("wobble") | express.js:20:34:20:38 | taint | This code execution depends on a $@. | express.js:19:17:19:35 | req.param("wobble") | user-provided value | | express.js:36:15:36:19 | taint | express.js:27:17:27:35 | req.param("wobble") | express.js:36:15:36:19 | taint | This code execution depends on a $@. | express.js:27:17:27:35 | req.param("wobble") | user-provided value | | express.js:43:10:43:12 | msg | express.js:42:30:42:32 | msg | express.js:43:10:43:12 | msg | This code execution depends on a $@. | express.js:42:30:42:32 | msg | user-provided value | +| fastify.js:5:44:5:52 | userInput | fastify.js:4:21:4:33 | request.query | fastify.js:5:44:5:52 | userInput | This code execution depends on a $@. | fastify.js:4:21:4:33 | request.query | user-provided value | +| fastify.js:5:44:5:52 | userInput | fastify.js:4:21:4:43 | request ... Request | fastify.js:5:44:5:52 | userInput | This code execution depends on a $@. | fastify.js:4:21:4:43 | request ... Request | user-provided value | +| fastify.js:10:44:10:52 | userInput | fastify.js:9:21:9:33 | request.query | fastify.js:10:44:10:52 | userInput | This code execution depends on a $@. | fastify.js:9:21:9:33 | request.query | user-provided value | +| fastify.js:10:44:10:52 | userInput | fastify.js:9:21:9:40 | request.query.onSend | fastify.js:10:44:10:52 | userInput | This code execution depends on a $@. | fastify.js:9:21:9:40 | request.query.onSend | user-provided value | +| fastify.js:16:44:16:52 | userInput | fastify.js:15:21:15:33 | request.query | fastify.js:16:44:16:52 | userInput | This code execution depends on a $@. | fastify.js:15:21:15:33 | request.query | user-provided value | +| fastify.js:16:44:16:52 | userInput | fastify.js:15:21:15:44 | request ... Parsing | fastify.js:16:44:16:52 | userInput | This code execution depends on a $@. | fastify.js:15:21:15:44 | request ... Parsing | user-provided value | +| fastify.js:22:44:22:52 | userInput | fastify.js:21:21:21:33 | request.query | fastify.js:22:44:22:52 | userInput | This code execution depends on a $@. | fastify.js:21:21:21:33 | request.query | user-provided value | +| fastify.js:22:44:22:52 | userInput | fastify.js:21:21:21:47 | request ... idation | fastify.js:22:44:22:52 | userInput | This code execution depends on a $@. | fastify.js:21:21:21:47 | request ... idation | user-provided value | +| fastify.js:27:44:27:52 | userInput | fastify.js:26:21:26:33 | request.query | fastify.js:27:44:27:52 | userInput | This code execution depends on a $@. | fastify.js:26:21:26:33 | request.query | user-provided value | +| fastify.js:27:44:27:52 | userInput | fastify.js:26:21:26:44 | request ... Handler | fastify.js:27:44:27:52 | userInput | This code execution depends on a $@. | fastify.js:26:21:26:44 | request ... Handler | user-provided value | +| fastify.js:32:44:32:52 | userInput | fastify.js:31:21:31:33 | request.query | fastify.js:32:44:32:52 | userInput | This code execution depends on a $@. | fastify.js:31:21:31:33 | request.query | user-provided value | +| fastify.js:32:44:32:52 | userInput | fastify.js:31:21:31:50 | request ... ization | fastify.js:32:44:32:52 | userInput | This code execution depends on a $@. | fastify.js:31:21:31:50 | request ... ization | user-provided value | +| fastify.js:38:44:38:52 | userInput | fastify.js:37:21:37:33 | request.query | fastify.js:38:44:38:52 | userInput | This code execution depends on a $@. | fastify.js:37:21:37:33 | request.query | user-provided value | +| fastify.js:38:44:38:52 | userInput | fastify.js:37:21:37:44 | request ... esponse | fastify.js:38:44:38:52 | userInput | This code execution depends on a $@. | fastify.js:37:21:37:44 | request ... esponse | user-provided value | +| fastify.js:43:44:43:52 | userInput | fastify.js:42:21:42:33 | request.query | fastify.js:43:44:43:52 | userInput | This code execution depends on a $@. | fastify.js:42:21:42:33 | request.query | user-provided value | +| fastify.js:43:44:43:52 | userInput | fastify.js:42:21:42:41 | request ... onError | fastify.js:43:44:43:52 | userInput | This code execution depends on a $@. | fastify.js:42:21:42:41 | request ... onError | user-provided value | +| fastify.js:48:44:48:52 | userInput | fastify.js:47:21:47:33 | request.query | fastify.js:48:44:48:52 | userInput | This code execution depends on a $@. | fastify.js:47:21:47:33 | request.query | user-provided value | +| fastify.js:48:44:48:52 | userInput | fastify.js:47:21:47:43 | request ... Timeout | fastify.js:48:44:48:52 | userInput | This code execution depends on a $@. | fastify.js:47:21:47:43 | request ... Timeout | user-provided value | +| fastify.js:53:46:53:54 | userInput | fastify.js:52:23:52:35 | request.query | fastify.js:53:46:53:54 | userInput | This code execution depends on a $@. | fastify.js:52:23:52:35 | request.query | user-provided value | +| fastify.js:53:46:53:54 | userInput | fastify.js:52:23:52:50 | request ... stAbort | fastify.js:53:46:53:54 | userInput | This code execution depends on a $@. | fastify.js:52:23:52:50 | request ... stAbort | user-provided value | | fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value | | fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | | fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value | @@ -79,6 +99,36 @@ edges | express.js:27:9:27:35 | taint | express.js:36:15:36:19 | taint | provenance | | | express.js:27:17:27:35 | req.param("wobble") | express.js:27:9:27:35 | taint | provenance | | | express.js:42:30:42:32 | msg | express.js:43:10:43:12 | msg | provenance | | +| fastify.js:4:9:4:43 | userInput | fastify.js:5:44:5:52 | userInput | provenance | | +| fastify.js:4:21:4:33 | request.query | fastify.js:4:9:4:43 | userInput | provenance | | +| fastify.js:4:21:4:43 | request ... Request | fastify.js:4:9:4:43 | userInput | provenance | | +| fastify.js:9:9:9:40 | userInput | fastify.js:10:44:10:52 | userInput | provenance | | +| fastify.js:9:21:9:33 | request.query | fastify.js:9:9:9:40 | userInput | provenance | | +| fastify.js:9:21:9:40 | request.query.onSend | fastify.js:9:9:9:40 | userInput | provenance | | +| fastify.js:15:9:15:44 | userInput | fastify.js:16:44:16:52 | userInput | provenance | | +| fastify.js:15:21:15:33 | request.query | fastify.js:15:9:15:44 | userInput | provenance | | +| fastify.js:15:21:15:44 | request ... Parsing | fastify.js:15:9:15:44 | userInput | provenance | | +| fastify.js:21:9:21:47 | userInput | fastify.js:22:44:22:52 | userInput | provenance | | +| fastify.js:21:21:21:33 | request.query | fastify.js:21:9:21:47 | userInput | provenance | | +| fastify.js:21:21:21:47 | request ... idation | fastify.js:21:9:21:47 | userInput | provenance | | +| fastify.js:26:9:26:44 | userInput | fastify.js:27:44:27:52 | userInput | provenance | | +| fastify.js:26:21:26:33 | request.query | fastify.js:26:9:26:44 | userInput | provenance | | +| fastify.js:26:21:26:44 | request ... Handler | fastify.js:26:9:26:44 | userInput | provenance | | +| fastify.js:31:9:31:50 | userInput | fastify.js:32:44:32:52 | userInput | provenance | | +| fastify.js:31:21:31:33 | request.query | fastify.js:31:9:31:50 | userInput | provenance | | +| fastify.js:31:21:31:50 | request ... ization | fastify.js:31:9:31:50 | userInput | provenance | | +| fastify.js:37:9:37:44 | userInput | fastify.js:38:44:38:52 | userInput | provenance | | +| fastify.js:37:21:37:33 | request.query | fastify.js:37:9:37:44 | userInput | provenance | | +| fastify.js:37:21:37:44 | request ... esponse | fastify.js:37:9:37:44 | userInput | provenance | | +| fastify.js:42:9:42:41 | userInput | fastify.js:43:44:43:52 | userInput | provenance | | +| fastify.js:42:21:42:33 | request.query | fastify.js:42:9:42:41 | userInput | provenance | | +| fastify.js:42:21:42:41 | request ... onError | fastify.js:42:9:42:41 | userInput | provenance | | +| fastify.js:47:9:47:43 | userInput | fastify.js:48:44:48:52 | userInput | provenance | | +| fastify.js:47:21:47:33 | request.query | fastify.js:47:9:47:43 | userInput | provenance | | +| fastify.js:47:21:47:43 | request ... Timeout | fastify.js:47:9:47:43 | userInput | provenance | | +| fastify.js:52:11:52:50 | userInput | fastify.js:53:46:53:54 | userInput | provenance | | +| fastify.js:52:23:52:35 | request.query | fastify.js:52:11:52:50 | userInput | provenance | | +| fastify.js:52:23:52:50 | request ... stAbort | fastify.js:52:11:52:50 | userInput | provenance | | | fastify.js:57:9:57:39 | userInput | fastify.js:58:44:58:52 | userInput | provenance | | | fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | | | fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | | @@ -152,6 +202,46 @@ nodes | express.js:36:15:36:19 | taint | semmle.label | taint | | express.js:42:30:42:32 | msg | semmle.label | msg | | express.js:43:10:43:12 | msg | semmle.label | msg | +| fastify.js:4:9:4:43 | userInput | semmle.label | userInput | +| fastify.js:4:21:4:33 | request.query | semmle.label | request.query | +| fastify.js:4:21:4:43 | request ... Request | semmle.label | request ... Request | +| fastify.js:5:44:5:52 | userInput | semmle.label | userInput | +| fastify.js:9:9:9:40 | userInput | semmle.label | userInput | +| fastify.js:9:21:9:33 | request.query | semmle.label | request.query | +| fastify.js:9:21:9:40 | request.query.onSend | semmle.label | request.query.onSend | +| fastify.js:10:44:10:52 | userInput | semmle.label | userInput | +| fastify.js:15:9:15:44 | userInput | semmle.label | userInput | +| fastify.js:15:21:15:33 | request.query | semmle.label | request.query | +| fastify.js:15:21:15:44 | request ... Parsing | semmle.label | request ... Parsing | +| fastify.js:16:44:16:52 | userInput | semmle.label | userInput | +| fastify.js:21:9:21:47 | userInput | semmle.label | userInput | +| fastify.js:21:21:21:33 | request.query | semmle.label | request.query | +| fastify.js:21:21:21:47 | request ... idation | semmle.label | request ... idation | +| fastify.js:22:44:22:52 | userInput | semmle.label | userInput | +| fastify.js:26:9:26:44 | userInput | semmle.label | userInput | +| fastify.js:26:21:26:33 | request.query | semmle.label | request.query | +| fastify.js:26:21:26:44 | request ... Handler | semmle.label | request ... Handler | +| fastify.js:27:44:27:52 | userInput | semmle.label | userInput | +| fastify.js:31:9:31:50 | userInput | semmle.label | userInput | +| fastify.js:31:21:31:33 | request.query | semmle.label | request.query | +| fastify.js:31:21:31:50 | request ... ization | semmle.label | request ... ization | +| fastify.js:32:44:32:52 | userInput | semmle.label | userInput | +| fastify.js:37:9:37:44 | userInput | semmle.label | userInput | +| fastify.js:37:21:37:33 | request.query | semmle.label | request.query | +| fastify.js:37:21:37:44 | request ... esponse | semmle.label | request ... esponse | +| fastify.js:38:44:38:52 | userInput | semmle.label | userInput | +| fastify.js:42:9:42:41 | userInput | semmle.label | userInput | +| fastify.js:42:21:42:33 | request.query | semmle.label | request.query | +| fastify.js:42:21:42:41 | request ... onError | semmle.label | request ... onError | +| fastify.js:43:44:43:52 | userInput | semmle.label | userInput | +| fastify.js:47:9:47:43 | userInput | semmle.label | userInput | +| fastify.js:47:21:47:33 | request.query | semmle.label | request.query | +| fastify.js:47:21:47:43 | request ... Timeout | semmle.label | request ... Timeout | +| fastify.js:48:44:48:52 | userInput | semmle.label | userInput | +| fastify.js:52:11:52:50 | userInput | semmle.label | userInput | +| fastify.js:52:23:52:35 | request.query | semmle.label | request.query | +| fastify.js:52:23:52:50 | request ... stAbort | semmle.label | request ... stAbort | +| fastify.js:53:46:53:54 | userInput | semmle.label | userInput | | fastify.js:57:9:57:39 | userInput | semmle.label | userInput | | fastify.js:57:21:57:33 | request.query | semmle.label | request.query | | fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected index b56f28a79171..41550a22c9f7 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected @@ -11,6 +11,36 @@ edges | express.js:27:9:27:35 | taint | express.js:36:15:36:19 | taint | provenance | | | express.js:27:17:27:35 | req.param("wobble") | express.js:27:9:27:35 | taint | provenance | | | express.js:42:30:42:32 | msg | express.js:43:10:43:12 | msg | provenance | | +| fastify.js:4:9:4:43 | userInput | fastify.js:5:44:5:52 | userInput | provenance | | +| fastify.js:4:21:4:33 | request.query | fastify.js:4:9:4:43 | userInput | provenance | | +| fastify.js:4:21:4:43 | request ... Request | fastify.js:4:9:4:43 | userInput | provenance | | +| fastify.js:9:9:9:40 | userInput | fastify.js:10:44:10:52 | userInput | provenance | | +| fastify.js:9:21:9:33 | request.query | fastify.js:9:9:9:40 | userInput | provenance | | +| fastify.js:9:21:9:40 | request.query.onSend | fastify.js:9:9:9:40 | userInput | provenance | | +| fastify.js:15:9:15:44 | userInput | fastify.js:16:44:16:52 | userInput | provenance | | +| fastify.js:15:21:15:33 | request.query | fastify.js:15:9:15:44 | userInput | provenance | | +| fastify.js:15:21:15:44 | request ... Parsing | fastify.js:15:9:15:44 | userInput | provenance | | +| fastify.js:21:9:21:47 | userInput | fastify.js:22:44:22:52 | userInput | provenance | | +| fastify.js:21:21:21:33 | request.query | fastify.js:21:9:21:47 | userInput | provenance | | +| fastify.js:21:21:21:47 | request ... idation | fastify.js:21:9:21:47 | userInput | provenance | | +| fastify.js:26:9:26:44 | userInput | fastify.js:27:44:27:52 | userInput | provenance | | +| fastify.js:26:21:26:33 | request.query | fastify.js:26:9:26:44 | userInput | provenance | | +| fastify.js:26:21:26:44 | request ... Handler | fastify.js:26:9:26:44 | userInput | provenance | | +| fastify.js:31:9:31:50 | userInput | fastify.js:32:44:32:52 | userInput | provenance | | +| fastify.js:31:21:31:33 | request.query | fastify.js:31:9:31:50 | userInput | provenance | | +| fastify.js:31:21:31:50 | request ... ization | fastify.js:31:9:31:50 | userInput | provenance | | +| fastify.js:37:9:37:44 | userInput | fastify.js:38:44:38:52 | userInput | provenance | | +| fastify.js:37:21:37:33 | request.query | fastify.js:37:9:37:44 | userInput | provenance | | +| fastify.js:37:21:37:44 | request ... esponse | fastify.js:37:9:37:44 | userInput | provenance | | +| fastify.js:42:9:42:41 | userInput | fastify.js:43:44:43:52 | userInput | provenance | | +| fastify.js:42:21:42:33 | request.query | fastify.js:42:9:42:41 | userInput | provenance | | +| fastify.js:42:21:42:41 | request ... onError | fastify.js:42:9:42:41 | userInput | provenance | | +| fastify.js:47:9:47:43 | userInput | fastify.js:48:44:48:52 | userInput | provenance | | +| fastify.js:47:21:47:33 | request.query | fastify.js:47:9:47:43 | userInput | provenance | | +| fastify.js:47:21:47:43 | request ... Timeout | fastify.js:47:9:47:43 | userInput | provenance | | +| fastify.js:52:11:52:50 | userInput | fastify.js:53:46:53:54 | userInput | provenance | | +| fastify.js:52:23:52:35 | request.query | fastify.js:52:11:52:50 | userInput | provenance | | +| fastify.js:52:23:52:50 | request ... stAbort | fastify.js:52:11:52:50 | userInput | provenance | | | fastify.js:57:9:57:39 | userInput | fastify.js:58:44:58:52 | userInput | provenance | | | fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | | | fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | | @@ -86,6 +116,46 @@ nodes | express.js:36:15:36:19 | taint | semmle.label | taint | | express.js:42:30:42:32 | msg | semmle.label | msg | | express.js:43:10:43:12 | msg | semmle.label | msg | +| fastify.js:4:9:4:43 | userInput | semmle.label | userInput | +| fastify.js:4:21:4:33 | request.query | semmle.label | request.query | +| fastify.js:4:21:4:43 | request ... Request | semmle.label | request ... Request | +| fastify.js:5:44:5:52 | userInput | semmle.label | userInput | +| fastify.js:9:9:9:40 | userInput | semmle.label | userInput | +| fastify.js:9:21:9:33 | request.query | semmle.label | request.query | +| fastify.js:9:21:9:40 | request.query.onSend | semmle.label | request.query.onSend | +| fastify.js:10:44:10:52 | userInput | semmle.label | userInput | +| fastify.js:15:9:15:44 | userInput | semmle.label | userInput | +| fastify.js:15:21:15:33 | request.query | semmle.label | request.query | +| fastify.js:15:21:15:44 | request ... Parsing | semmle.label | request ... Parsing | +| fastify.js:16:44:16:52 | userInput | semmle.label | userInput | +| fastify.js:21:9:21:47 | userInput | semmle.label | userInput | +| fastify.js:21:21:21:33 | request.query | semmle.label | request.query | +| fastify.js:21:21:21:47 | request ... idation | semmle.label | request ... idation | +| fastify.js:22:44:22:52 | userInput | semmle.label | userInput | +| fastify.js:26:9:26:44 | userInput | semmle.label | userInput | +| fastify.js:26:21:26:33 | request.query | semmle.label | request.query | +| fastify.js:26:21:26:44 | request ... Handler | semmle.label | request ... Handler | +| fastify.js:27:44:27:52 | userInput | semmle.label | userInput | +| fastify.js:31:9:31:50 | userInput | semmle.label | userInput | +| fastify.js:31:21:31:33 | request.query | semmle.label | request.query | +| fastify.js:31:21:31:50 | request ... ization | semmle.label | request ... ization | +| fastify.js:32:44:32:52 | userInput | semmle.label | userInput | +| fastify.js:37:9:37:44 | userInput | semmle.label | userInput | +| fastify.js:37:21:37:33 | request.query | semmle.label | request.query | +| fastify.js:37:21:37:44 | request ... esponse | semmle.label | request ... esponse | +| fastify.js:38:44:38:52 | userInput | semmle.label | userInput | +| fastify.js:42:9:42:41 | userInput | semmle.label | userInput | +| fastify.js:42:21:42:33 | request.query | semmle.label | request.query | +| fastify.js:42:21:42:41 | request ... onError | semmle.label | request ... onError | +| fastify.js:43:44:43:52 | userInput | semmle.label | userInput | +| fastify.js:47:9:47:43 | userInput | semmle.label | userInput | +| fastify.js:47:21:47:33 | request.query | semmle.label | request.query | +| fastify.js:47:21:47:43 | request ... Timeout | semmle.label | request ... Timeout | +| fastify.js:48:44:48:52 | userInput | semmle.label | userInput | +| fastify.js:52:11:52:50 | userInput | semmle.label | userInput | +| fastify.js:52:23:52:35 | request.query | semmle.label | request.query | +| fastify.js:52:23:52:50 | request ... stAbort | semmle.label | request ... stAbort | +| fastify.js:53:46:53:54 | userInput | semmle.label | userInput | | fastify.js:57:9:57:39 | userInput | semmle.label | userInput | | fastify.js:57:21:57:33 | request.query | semmle.label | request.query | | fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js index b8878cdab41c..082061bba3f8 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js @@ -1,56 +1,56 @@ const fastify = require('fastify')({ logger: true }); fastify.addHook('onRequest', async (request, reply) => { - const userInput = request.query.onRequest; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.onRequest; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.addHook('onSend', async (request, reply, payload) => { - const userInput = request.query.onSend; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.onSend; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] return JSON.stringify({ ...JSON.parse(payload), onSend: request.evalResult }); }); fastify.addHook('preParsing', async (request, reply, payload) => { - const userInput = request.query.preParsing; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.preParsing; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] return payload; }); fastify.addHook('preValidation', async (request, reply) => { - const userInput = request.query.preValidation; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.preValidation; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.addHook('preHandler', async (request, reply) => { - const userInput = request.query.preHandler; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.preHandler; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.addHook('preSerialization', async (request, reply, payload) => { - const userInput = request.query.preSerialization; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.preSerialization; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] return payload; }); fastify.addHook('onResponse', async (request, reply) => { - const userInput = request.query.onResponse; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.onResponse; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.addHook('onError', async (request, reply, error) => { - const userInput = request.query.onError; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.onError; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.addHook('onTimeout', async (request, reply) => { - const userInput = request.query.onTimeout; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.onTimeout; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.addHook('onRequestAbort', (request, done) => { - const userInput = request.query.onRequestAbort; // $ MISSING: Source[js/code-injection] - if (userInput) request.evalResult = eval(userInput); // $ MISSING: Alert[js/code-injection] + const userInput = request.query.onRequestAbort; // $ Source[js/code-injection] + if (userInput) request.evalResult = eval(userInput); // $ Alert[js/code-injection] }); fastify.get('/dangerous', async (request, reply) => { From 5c3556da66d7d28d287ec475d51b51dd043aa9b7 Mon Sep 17 00:00:00 2001 From: Napalys Date: Tue, 15 Apr 2025 09:41:52 +0200 Subject: [PATCH 4/7] Add user-controlled property tracking and update code injection alerts in Fastify hooks --- .../semmle/javascript/frameworks/Fastify.qll | 28 +++++++++++++++++++ .../CodeInjection/CodeInjection.expected | 6 ++++ .../HeuristicSourceCodeInjection.expected | 3 ++ .../Security/CWE-094/CodeInjection/fastify.js | 6 ++-- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll index 5b2485c76794..53c92e7f7b33 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll @@ -238,6 +238,20 @@ module Fastify { } } + /** + * Gets the property name where user-controlled input is written to a request or response object + * in a route handler. This is used to track taint flow through request and response object properties. + */ + private string getUserControlledPropertyName() { + exists(DataFlow::PropWrite write, DataFlow::Node source, RouteHandler rh | + write.getBase*() = + [rh.getARequestSource().ref().getALocalUse(), rh.getAResponseSource().ref().getALocalUse()] and + write.getPropertyName() = result and + write.getRhs() = source and + source = any(Http::RequestInputAccess ria).getASuccessor*() + ) + } + /** * An access to a user-controlled Fastify request input. */ @@ -252,6 +266,20 @@ module Fastify { or kind = "body" and name = "body" + or + kind = "stored" and + name = getUserControlledPropertyName() + ) + or + // Handle reading from reply object with user input stored on it + exists(string name | + ( + this = rh.getAResponseSource().ref().getAPropertyRead(name) + or + this = rh.getAResponseSource().ref().getAPropertyRead+().getAPropertyRead(name) + ) and + kind = "stored" and + name = getUserControlledPropertyName() ) } diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected index 34487a43e630..9a81aa80a2c0 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected @@ -51,6 +51,9 @@ | fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | | fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value | | fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | +| fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:71:34:71:51 | request.storedCode | user-provided value | +| fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:84:30:84:43 | reply.userCode | user-provided value | +| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:99:30:99:52 | reply.l ... tedCode | user-provided value | | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | This code execution depends on a $@. | module.js:9:16:9:29 | req.query.code | user-provided value | | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | This code execution depends on a $@. | module.js:11:17:11:30 | req.query.code | user-provided value | | react-native.js:8:32:8:38 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:8:32:8:38 | tainted | This code execution depends on a $@. | react-native.js:7:17:7:33 | req.param("code") | user-provided value | @@ -247,6 +250,9 @@ nodes | fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | | fastify.js:58:44:58:52 | userInput | semmle.label | userInput | | fastify.js:59:23:59:31 | userInput | semmle.label | userInput | +| fastify.js:71:34:71:51 | request.storedCode | semmle.label | request.storedCode | +| fastify.js:84:30:84:43 | reply.userCode | semmle.label | reply.userCode | +| fastify.js:99:30:99:52 | reply.l ... tedCode | semmle.label | reply.l ... tedCode | | module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code | | module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code | | react-native.js:7:7:7:33 | tainted | semmle.label | tainted | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected index 41550a22c9f7..0f52acfa6cde 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected @@ -161,6 +161,9 @@ nodes | fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | | fastify.js:58:44:58:52 | userInput | semmle.label | userInput | | fastify.js:59:23:59:31 | userInput | semmle.label | userInput | +| fastify.js:71:34:71:51 | request.storedCode | semmle.label | request.storedCode | +| fastify.js:84:30:84:43 | reply.userCode | semmle.label | reply.userCode | +| fastify.js:99:30:99:52 | reply.l ... tedCode | semmle.label | reply.l ... tedCode | | module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code | | module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code | | react-native.js:7:7:7:33 | tainted | semmle.label | tainted | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js index 082061bba3f8..bb5577c7acc1 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js @@ -68,7 +68,7 @@ fastify.addHook('preHandler', async (request, reply) => { fastify.get('/flow-through-request', async (request, reply) => { // Use the stored code from previous hook if (request.storedCode) { - const evaluatedResult = eval(request.storedCode); // $ MISSING: Alert[js/code-injection] + const evaluatedResult = eval(request.storedCode); // $ Alert[js/code-injection] return { result: evaluatedResult }; } return { result: null }; @@ -81,7 +81,7 @@ fastify.addHook('onRequest', async (request, reply) => { fastify.get('/flow-through-reply', async (request, reply) => { // Use the code stored in reply object if (reply.userCode) { - const replyResult = eval(reply.userCode); // $ MISSING: Alert[js/code-injection] + const replyResult = eval(reply.userCode); // $ Alert[js/code-injection] return { result: replyResult }; } return { result: null }; @@ -96,7 +96,7 @@ fastify.addHook('onRequest', async (request, reply) => { fastify.get('/flow-through-reply', async (request, reply) => { // Use the code stored in reply object if (reply.locals && reply.locals.nestedCode) { - const replyResult = eval(reply.locals.nestedCode); // $ MISSING: Alert[js/code-injection] + const replyResult = eval(reply.locals.nestedCode); // $ Alert[js/code-injection] return { result: replyResult }; } return { result: null }; From 00661b62dc489e08716e6c713ce2ca7dac66a07c Mon Sep 17 00:00:00 2001 From: Asger F Date: Tue, 22 Apr 2025 12:00:02 +0200 Subject: [PATCH 5/7] JS: Add isMiddlewareSetup() hook to Routing model --- .../ql/lib/semmle/javascript/Routing.qll | 39 ++++++++++++++++++- .../semmle/javascript/frameworks/Fastify.qll | 8 +++- .../CodeInjection/CodeInjection.expected | 18 +++++++++ .../HeuristicSourceCodeInjection.expected | 12 ++++++ .../Security/CWE-094/CodeInjection/fastify.js | 6 +-- 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/Routing.qll b/javascript/ql/lib/semmle/javascript/Routing.qll index 29700a255d61..530322a2d2c4 100644 --- a/javascript/ql/lib/semmle/javascript/Routing.qll +++ b/javascript/ql/lib/semmle/javascript/Routing.qll @@ -139,6 +139,8 @@ module Routing { predicate mayResumeDispatch() { this.getLastChild().mayResumeDispatch() or + isInMiddlewareSetup(this) + or exists(this.(RouteHandler).getAContinuationInvocation()) or // Leaf nodes that aren't functions are assumed to invoke their continuation @@ -155,6 +157,8 @@ module Routing { predicate definitelyResumesDispatch() { this.getLastChild().definitelyResumesDispatch() or + isInMiddlewareSetup(this) + or exists(this.(RouteHandler).getAContinuationInvocation()) or this instanceof MkRouter @@ -325,6 +329,19 @@ module Routing { DataFlow::Node getValueImplicitlyStoredInAccessPath(int n, string path) { none() } } + /** + * Holds if `node` is installed at a route handler that is declared to be a middleware setup, + * and is therefore assume to resume dispatch. + */ + private predicate isInMiddlewareSetup(Node node) { + exists(RouteSetup::Range range | + node = getRouteSetupNode(range) and + range.isMiddlewareSetup() + ) + or + isInMiddlewareSetup(node.getParent()) + } + /** Holds if `pred` and `succ` are adjacent siblings and `succ` is installed after `pred`. */ private predicate areSiblings(Node pred, Node succ) { exists(ValueNode::Range base, int n | @@ -612,6 +629,20 @@ module Routing { * Holds if this route setup targets `router` and occurs at the given `cfgNode`. */ abstract predicate isInstalledAt(Router::Range router, ControlFlowNode cfgNode); + + /** + * Holds if this is a middleware setup, meaning dispatch will resume after the + * route handlers in this route setup have completed (usually meaning that they have returned a promise, which has resolved). + * + * This should only be overridden when the route setup itself determines whether subsequent + * route handlers are invoked afterwards. + * - For Express-like libraries, the route _handler_ determines whether to resume dispatch, + * based on whether the `next` callback is invoked. For such libraries, do not override `isMiddlewareSetup`. + * - For Fastify-like libraries, the route _setup_ determines whether to resume dispatch. + * For example, `.addHook()` will resume dispatch whereas `.get()` will not. `isMiddlewareSetup()` should thus + * hold for `.addHook()` but not for `.get()` calls. + */ + predicate isMiddlewareSetup() { none() } } /** @@ -892,10 +923,14 @@ module Routing { * based on `Node::Range::getValueAtAccessPath`. */ private DataFlow::Node getAnAccessPathRhs(Node base, int n, string path) { - // Assigned in the body of a route handler function, whi + // Assigned in the body of a route handler function, which is a middleware exists(RouteHandler handler | base = handler | result = AccessPath::getAnAssignmentTo(handler.getParameter(n).ref(), path) and - exists(handler.getAContinuationInvocation()) + ( + exists(handler.getAContinuationInvocation()) + or + isInMiddlewareSetup(handler) + ) ) or // Implicit assignment contributed by framework model diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll index 53c92e7f7b33..2c2f0f6fc114 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll @@ -164,13 +164,19 @@ module Fastify { private class ShorthandRoutingTreeSetup extends Routing::RouteSetup::MethodCall instanceof RouteSetup { - ShorthandRoutingTreeSetup() { not this.getMethodName() = "route" } + ShorthandRoutingTreeSetup() { not this.getMethodName() = ["route", "addHook"] } override string getRelativePath() { result = this.getArgument(0).getStringValue() } override Http::RequestMethodName getHttpMethod() { result = this.getMethodName().toUpperCase() } } + private class AddHookRouteSetup extends Routing::RouteSetup::MethodCall instanceof RouteSetup { + AddHookRouteSetup() { this.getMethodName() = "addHook" } + + override predicate isMiddlewareSetup() { any() } + } + /** Gets the name of the `n`th handler function that can be installed a route setup, in order of execution. */ private string getNthHandlerName(int n) { result = diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected index 9a81aa80a2c0..a63cc195ecbd 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected @@ -51,8 +51,14 @@ | fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | | fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value | | fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | +| fastify.js:71:34:71:51 | request.storedCode | fastify.js:66:24:66:36 | request.query | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:66:24:66:36 | request.query | user-provided value | +| fastify.js:71:34:71:51 | request.storedCode | fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:66:24:66:47 | request ... redCode | user-provided value | | fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:71:34:71:51 | request.storedCode | user-provided value | +| fastify.js:84:30:84:43 | reply.userCode | fastify.js:79:20:79:32 | request.query | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:79:20:79:32 | request.query | user-provided value | +| fastify.js:84:30:84:43 | reply.userCode | fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:79:20:79:42 | request ... plyCode | user-provided value | | fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:84:30:84:43 | reply.userCode | user-provided value | +| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:94:29:94:41 | request.query | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:94:29:94:41 | request.query | user-provided value | +| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:94:29:94:51 | request ... plyCode | user-provided value | | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:99:30:99:52 | reply.l ... tedCode | user-provided value | | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | This code execution depends on a $@. | module.js:9:16:9:29 | req.query.code | user-provided value | | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | This code execution depends on a $@. | module.js:11:17:11:30 | req.query.code | user-provided value | @@ -136,6 +142,12 @@ edges | fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | | | fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | | | fastify.js:57:21:57:39 | request.query.input | fastify.js:57:9:57:39 | userInput | provenance | | +| fastify.js:66:24:66:36 | request.query | fastify.js:66:24:66:47 | request ... redCode | provenance | | +| fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | provenance | | +| fastify.js:79:20:79:32 | request.query | fastify.js:79:20:79:42 | request ... plyCode | provenance | | +| fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | provenance | | +| fastify.js:94:29:94:41 | request.query | fastify.js:94:29:94:51 | request ... plyCode | provenance | | +| fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted | provenance | | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | provenance | | @@ -250,8 +262,14 @@ nodes | fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | | fastify.js:58:44:58:52 | userInput | semmle.label | userInput | | fastify.js:59:23:59:31 | userInput | semmle.label | userInput | +| fastify.js:66:24:66:36 | request.query | semmle.label | request.query | +| fastify.js:66:24:66:47 | request ... redCode | semmle.label | request ... redCode | | fastify.js:71:34:71:51 | request.storedCode | semmle.label | request.storedCode | +| fastify.js:79:20:79:32 | request.query | semmle.label | request.query | +| fastify.js:79:20:79:42 | request ... plyCode | semmle.label | request ... plyCode | | fastify.js:84:30:84:43 | reply.userCode | semmle.label | reply.userCode | +| fastify.js:94:29:94:41 | request.query | semmle.label | request.query | +| fastify.js:94:29:94:51 | request ... plyCode | semmle.label | request ... plyCode | | fastify.js:99:30:99:52 | reply.l ... tedCode | semmle.label | reply.l ... tedCode | | module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code | | module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected index 0f52acfa6cde..aa23d7a6d5a1 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected @@ -45,6 +45,12 @@ edges | fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | | | fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | | | fastify.js:57:21:57:39 | request.query.input | fastify.js:57:9:57:39 | userInput | provenance | | +| fastify.js:66:24:66:36 | request.query | fastify.js:66:24:66:47 | request ... redCode | provenance | | +| fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | provenance | | +| fastify.js:79:20:79:32 | request.query | fastify.js:79:20:79:42 | request ... plyCode | provenance | | +| fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | provenance | | +| fastify.js:94:29:94:41 | request.query | fastify.js:94:29:94:51 | request ... plyCode | provenance | | +| fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted | provenance | | | react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted | provenance | | | react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | provenance | | @@ -161,8 +167,14 @@ nodes | fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input | | fastify.js:58:44:58:52 | userInput | semmle.label | userInput | | fastify.js:59:23:59:31 | userInput | semmle.label | userInput | +| fastify.js:66:24:66:36 | request.query | semmle.label | request.query | +| fastify.js:66:24:66:47 | request ... redCode | semmle.label | request ... redCode | | fastify.js:71:34:71:51 | request.storedCode | semmle.label | request.storedCode | +| fastify.js:79:20:79:32 | request.query | semmle.label | request.query | +| fastify.js:79:20:79:42 | request ... plyCode | semmle.label | request ... plyCode | | fastify.js:84:30:84:43 | reply.userCode | semmle.label | reply.userCode | +| fastify.js:94:29:94:41 | request.query | semmle.label | request.query | +| fastify.js:94:29:94:51 | request ... plyCode | semmle.label | request ... plyCode | | fastify.js:99:30:99:52 | reply.l ... tedCode | semmle.label | reply.l ... tedCode | | module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code | | module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code | diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js index bb5577c7acc1..e1cba0d277ca 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js @@ -63,7 +63,7 @@ fastify.get('/dangerous', async (request, reply) => { // Store user input in request object fastify.addHook('preHandler', async (request, reply) => { - request.storedCode = request.query.storedCode; + request.storedCode = request.query.storedCode; // $ Source[js/code-injection] }); fastify.get('/flow-through-request', async (request, reply) => { // Use the stored code from previous hook @@ -76,7 +76,7 @@ fastify.get('/flow-through-request', async (request, reply) => { // Store user input in reply object fastify.addHook('onRequest', async (request, reply) => { - reply.userCode = request.query.replyCode; + reply.userCode = request.query.replyCode; // $ Source[js/code-injection] }); fastify.get('/flow-through-reply', async (request, reply) => { // Use the code stored in reply object @@ -91,7 +91,7 @@ fastify.get('/flow-through-reply', async (request, reply) => { // Store user input in reply object fastify.addHook('onRequest', async (request, reply) => { reply.locals = reply.locals || {}; - reply.locals.nestedCode = request.query.replyCode; + reply.locals.nestedCode = request.query.replyCode; // $ Source[js/code-injection] }); fastify.get('/flow-through-reply', async (request, reply) => { // Use the code stored in reply object From fdfdcc0d9302ef2fe500aa125f3fc3d43aa9f3d6 Mon Sep 17 00:00:00 2001 From: Napalys Date: Tue, 22 Apr 2025 14:16:45 +0200 Subject: [PATCH 6/7] Undo unnecessary name tracking for request, response objects --- .../semmle/javascript/frameworks/Fastify.qll | 28 ------------------- .../CodeInjection/CodeInjection.expected | 3 -- 2 files changed, 31 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll index 2c2f0f6fc114..aaf1dbfdd770 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll @@ -244,20 +244,6 @@ module Fastify { } } - /** - * Gets the property name where user-controlled input is written to a request or response object - * in a route handler. This is used to track taint flow through request and response object properties. - */ - private string getUserControlledPropertyName() { - exists(DataFlow::PropWrite write, DataFlow::Node source, RouteHandler rh | - write.getBase*() = - [rh.getARequestSource().ref().getALocalUse(), rh.getAResponseSource().ref().getALocalUse()] and - write.getPropertyName() = result and - write.getRhs() = source and - source = any(Http::RequestInputAccess ria).getASuccessor*() - ) - } - /** * An access to a user-controlled Fastify request input. */ @@ -272,20 +258,6 @@ module Fastify { or kind = "body" and name = "body" - or - kind = "stored" and - name = getUserControlledPropertyName() - ) - or - // Handle reading from reply object with user input stored on it - exists(string name | - ( - this = rh.getAResponseSource().ref().getAPropertyRead(name) - or - this = rh.getAResponseSource().ref().getAPropertyRead+().getAPropertyRead(name) - ) and - kind = "stored" and - name = getUserControlledPropertyName() ) } diff --git a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected index a63cc195ecbd..9ad04af3a2c5 100644 --- a/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected @@ -53,13 +53,10 @@ | fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value | | fastify.js:71:34:71:51 | request.storedCode | fastify.js:66:24:66:36 | request.query | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:66:24:66:36 | request.query | user-provided value | | fastify.js:71:34:71:51 | request.storedCode | fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:66:24:66:47 | request ... redCode | user-provided value | -| fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:71:34:71:51 | request.storedCode | user-provided value | | fastify.js:84:30:84:43 | reply.userCode | fastify.js:79:20:79:32 | request.query | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:79:20:79:32 | request.query | user-provided value | | fastify.js:84:30:84:43 | reply.userCode | fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:79:20:79:42 | request ... plyCode | user-provided value | -| fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:84:30:84:43 | reply.userCode | user-provided value | | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:94:29:94:41 | request.query | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:94:29:94:41 | request.query | user-provided value | | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:94:29:94:51 | request ... plyCode | user-provided value | -| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:99:30:99:52 | reply.l ... tedCode | user-provided value | | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | This code execution depends on a $@. | module.js:9:16:9:29 | req.query.code | user-provided value | | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | This code execution depends on a $@. | module.js:11:17:11:30 | req.query.code | user-provided value | | react-native.js:8:32:8:38 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:8:32:8:38 | tainted | This code execution depends on a $@. | react-native.js:7:17:7:33 | req.param("code") | user-provided value | From 8b53f8f2a67cd456c48420c2ab950e506fa67c67 Mon Sep 17 00:00:00 2001 From: Napalys Klicius Date: Mon, 28 Apr 2025 12:21:21 +0200 Subject: [PATCH 7/7] Fix, prevent addHook return values from being treated as XSS sinks --- javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll index aaf1dbfdd770..cf8dd76b75e3 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll @@ -328,7 +328,11 @@ module Fastify { ResponseSendArgument() { this = rh.getAResponseSource().ref().getAMethodCall("send").getArgument(0) or - this = rh.(DataFlow::FunctionNode).getAReturn() + exists(RouteSetup setup | + rh = setup.getARouteHandler() and + this = rh.(DataFlow::FunctionNode).getAReturn() and + setup.getMethodName() != "addHook" + ) } override RouteHandler getRouteHandler() { result = rh }