From dfe05599d3792cf5b50c5a95759735383865cda1 Mon Sep 17 00:00:00 2001 From: murderteeth Date: Sun, 12 Apr 2026 17:33:25 +0000 Subject: [PATCH 1/6] JS: Add support for @vercel/node serverless functions This adds a framework model for Vercel serverless functions so that CodeQL's existing JavaScript security queries can detect vulnerabilities in handlers of the form export default function handler(req: VercelRequest, res: VercelResponse) { ... } Handlers are identified as the default export of a module whose first two parameters are typed as `VercelRequest`/`VercelResponse` from `@vercel/node`. The default-export constraint excludes private helpers that share the same signature. Type-based detection follows the same pattern already used by `NextReqResHandler` in `Next.qll`. The framework model covers: - Route handler recognition (default-exported typed handlers only) - Request input sources: `query`, `body`, `cookies`, and `url` (the last inherited from Node's `IncomingMessage`) - Named header accesses like `req.headers.host` and `req.headers.referer`, modelled as `Http::RequestHeaderAccess` so header-specific queries fire - Response sinks: `res.send`, `res.status(...).send`, `res.redirect` - Header definitions via `res.setHeader` Includes a library test exercising each model predicate (including a negative case for private helpers) and query consistency fixtures demonstrating end-to-end detection for js/reflected-xss, js/request-forgery, js/sql-injection, and js/command-line-injection. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../codeql/reusables/supported-frameworks.rst | 1 + .../change-notes/2026-04-12-vercel-node.md | 4 + javascript/ql/lib/javascript.qll | 1 + .../javascript/frameworks/VercelNode.qll | 200 ++++++++++++++++++ .../frameworks/vercel/HeaderDefinition.qll | 7 + .../frameworks/vercel/RedirectInvocation.qll | 7 + .../frameworks/vercel/RequestInputAccess.qll | 7 + .../frameworks/vercel/RequestSource.qll | 5 + .../vercel/ResponseSendArgument.qll | 7 + .../frameworks/vercel/ResponseSource.qll | 5 + .../frameworks/vercel/RouteHandler.qll | 3 + .../frameworks/vercel/src/notahandler.ts | 8 + .../frameworks/vercel/src/vercel.ts | 27 +++ .../frameworks/vercel/tests.expected | 22 ++ .../library-tests/frameworks/vercel/tests.ql | 7 + .../CommandInjection.expected | 11 + .../CWE-078/CommandInjection/vercel.ts | 9 + .../ReflectedXss/ReflectedXss.expected | 7 + .../ReflectedXssWithCustomSanitizer.expected | 2 + .../Security/CWE-079/ReflectedXss/vercel.ts | 6 + .../CWE-089/untyped/SqlInjection.expected | 11 + .../Security/CWE-089/untyped/vercel.ts | 10 + .../Security/CWE-918/RequestForgery.expected | 9 + .../query-tests/Security/CWE-918/vercel.ts | 7 + 24 files changed, 383 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2026-04-12-vercel-node.md create mode 100644 javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/HeaderDefinition.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/RedirectInvocation.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/RequestInputAccess.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/RequestSource.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/ResponseSendArgument.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/ResponseSource.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/RouteHandler.qll create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/tests.expected create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/tests.ql create mode 100644 javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/vercel.ts create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/vercel.ts create mode 100644 javascript/ql/test/query-tests/Security/CWE-089/untyped/vercel.ts create mode 100644 javascript/ql/test/query-tests/Security/CWE-918/vercel.ts diff --git a/docs/codeql/reusables/supported-frameworks.rst b/docs/codeql/reusables/supported-frameworks.rst index 472e463cf79b..581f34f92271 100644 --- a/docs/codeql/reusables/supported-frameworks.rst +++ b/docs/codeql/reusables/supported-frameworks.rst @@ -197,6 +197,7 @@ and the CodeQL library pack ``codeql/javascript-all`` (`changelog void`, where + * the types are imported from the `@vercel/node` package. The Vercel runtime + * invokes the default export for every incoming HTTP request. + */ +module VercelNode { + /** + * A Vercel serverless function handler, identified as the default export of a + * module whose first two parameters are typed as `VercelRequest` and + * `VercelResponse` from `@vercel/node`. + * + * Since `@vercel/node` is commonly imported as a type-only import, handlers + * are recognised by their TypeScript parameter types. The default-export + * constraint excludes private helpers or test utilities that share the + * same signature. + */ + class RouteHandler extends Http::Servers::StandardRouteHandler, DataFlow::FunctionNode { + DataFlow::ParameterNode req; + DataFlow::ParameterNode res; + + RouteHandler() { + this = any(Module m).getAnExportedValue("default").getAFunctionValue() and + req = this.getParameter(0) and + res = this.getParameter(1) and + req.hasUnderlyingType("@vercel/node", "VercelRequest") and + res.hasUnderlyingType("@vercel/node", "VercelResponse") + } + + /** Gets the parameter that contains the request object. */ + DataFlow::ParameterNode getRequest() { result = req } + + /** Gets the parameter that contains the response object. */ + DataFlow::ParameterNode getResponse() { result = res } + } + + /** + * A Vercel request source, that is, the request parameter of a route handler. + */ + private class RequestSource extends Http::Servers::RequestSource { + RouteHandler rh; + + RequestSource() { this = rh.getRequest() } + + override RouteHandler getRouteHandler() { result = rh } + } + + /** + * A Vercel response source, that is, the response parameter of a route handler. + */ + private class ResponseSource extends Http::Servers::ResponseSource { + RouteHandler rh; + + ResponseSource() { this = rh.getResponse() } + + override RouteHandler getRouteHandler() { result = rh } + } + + /** + * A chained response, such as `res.status(200)`, `res.type('html')`, or `res.set(...)`. + * + * These methods return the response object and are commonly chained before `send` or `json`. + */ + private class ChainedResponseSource extends Http::Servers::ResponseSource { + RouteHandler rh; + + ChainedResponseSource() { + exists(ResponseSource src | + this = src.ref().getAMethodCall(["status", "type", "set"]) and + rh = src.getRouteHandler() + ) + } + + override RouteHandler getRouteHandler() { result = rh } + } + + /** + * An access to user-controlled input on a Vercel request. + * + * Covers `req.query`, `req.body`, `req.cookies`, and `req.url` (inherited + * from Node's `IncomingMessage`). Named-header accesses like `req.headers.host` + * are handled by `RequestHeaderAccess` below. + */ + private class RequestInputAccess extends Http::RequestInputAccess { + RouteHandler rh; + string kind; + + RequestInputAccess() { + exists(RequestSource src | rh = src.getRouteHandler() | + this = src.ref().getAPropertyRead("query") and kind = "parameter" + or + this = src.ref().getAPropertyRead("body") and kind = "body" + or + this = src.ref().getAPropertyRead("cookies") and kind = "cookie" + or + this = src.ref().getAPropertyRead("url") and kind = "url" + ) + or + exists(RequestHeaderAccess access | this = access | + rh = access.getRouteHandler() and + kind = "header" + ) + } + + override RouteHandler getRouteHandler() { result = rh } + + override string getKind() { result = kind } + } + + /** + * An access to a named header on a Vercel request, for example + * `req.headers.host` or `req.headers.referer`. + */ + private class RequestHeaderAccess extends Http::RequestHeaderAccess { + RouteHandler rh; + + RequestHeaderAccess() { + exists(RequestSource src | + this = src.ref().getAPropertyRead("headers").getAPropertyRead() and + rh = src.getRouteHandler() + ) + } + + override string getAHeaderName() { + result = this.(DataFlow::PropRead).getPropertyName().toLowerCase() + } + + override RouteHandler getRouteHandler() { result = rh } + + override string getKind() { result = "header" } + } + + /** + * An argument to `res.send(...)` on a Vercel response, including chained + * calls such as `res.status(200).send(...)`. + */ + private class ResponseSendArgument extends Http::ResponseSendArgument { + RouteHandler rh; + + ResponseSendArgument() { + exists(Http::Servers::ResponseSource src | + (src instanceof ResponseSource or src instanceof ChainedResponseSource) and + this = src.ref().getAMethodCall("send").getArgument(0) and + rh = src.getRouteHandler() + ) + } + + override RouteHandler getRouteHandler() { result = rh } + } + + /** + * A call to `res.redirect(...)` on a Vercel response. + */ + private class RedirectInvocation extends Http::RedirectInvocation, DataFlow::MethodCallNode { + RouteHandler rh; + + RedirectInvocation() { + exists(ResponseSource src | + this = src.ref().getAMethodCall("redirect") and + rh = src.getRouteHandler() + ) + } + + override DataFlow::Node getUrlArgument() { result = this.getLastArgument() } + + override RouteHandler getRouteHandler() { result = rh } + } + + /** + * A call to `res.setHeader(name, value)` on a Vercel response. + */ + private class SetHeader extends Http::ExplicitHeaderDefinition, DataFlow::CallNode { + RouteHandler rh; + + SetHeader() { + exists(ResponseSource src | + this = src.ref().getAMethodCall("setHeader") and + rh = src.getRouteHandler() + ) + } + + override RouteHandler getRouteHandler() { result = rh } + + override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) { + headerName = this.getArgument(0).getStringValue().toLowerCase() and + headerValue = this.getArgument(1) + } + + override DataFlow::Node getNameNode() { result = this.getArgument(0) } + } +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/HeaderDefinition.qll b/javascript/ql/test/library-tests/frameworks/vercel/HeaderDefinition.qll new file mode 100644 index 000000000000..496ee6e32e81 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/HeaderDefinition.qll @@ -0,0 +1,7 @@ +import javascript + +query predicate test_HeaderDefinition( + Http::HeaderDefinition hd, string name, VercelNode::RouteHandler rh +) { + hd.getRouteHandler() = rh and name = hd.getAHeaderName() +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/RedirectInvocation.qll b/javascript/ql/test/library-tests/frameworks/vercel/RedirectInvocation.qll new file mode 100644 index 000000000000..76e37d4a77f3 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/RedirectInvocation.qll @@ -0,0 +1,7 @@ +import javascript + +query predicate test_RedirectInvocation( + Http::RedirectInvocation call, DataFlow::Node url, VercelNode::RouteHandler rh +) { + call.getRouteHandler() = rh and url = call.getUrlArgument() +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/RequestInputAccess.qll b/javascript/ql/test/library-tests/frameworks/vercel/RequestInputAccess.qll new file mode 100644 index 000000000000..ac91695d500d --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/RequestInputAccess.qll @@ -0,0 +1,7 @@ +import javascript + +query predicate test_RequestInputAccess( + Http::RequestInputAccess ria, string kind, VercelNode::RouteHandler rh +) { + ria.getRouteHandler() = rh and kind = ria.getKind() +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/RequestSource.qll b/javascript/ql/test/library-tests/frameworks/vercel/RequestSource.qll new file mode 100644 index 000000000000..a8bdcc010200 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/RequestSource.qll @@ -0,0 +1,5 @@ +import javascript + +query predicate test_RequestSource(Http::Servers::RequestSource src, VercelNode::RouteHandler rh) { + src.getRouteHandler() = rh +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/ResponseSendArgument.qll b/javascript/ql/test/library-tests/frameworks/vercel/ResponseSendArgument.qll new file mode 100644 index 000000000000..7795e5cfb737 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/ResponseSendArgument.qll @@ -0,0 +1,7 @@ +import javascript + +query predicate test_ResponseSendArgument( + Http::ResponseSendArgument arg, VercelNode::RouteHandler rh +) { + arg.getRouteHandler() = rh +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/ResponseSource.qll b/javascript/ql/test/library-tests/frameworks/vercel/ResponseSource.qll new file mode 100644 index 000000000000..3a734b02f139 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/ResponseSource.qll @@ -0,0 +1,5 @@ +import javascript + +query predicate test_ResponseSource(Http::Servers::ResponseSource src, VercelNode::RouteHandler rh) { + src.getRouteHandler() = rh +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/RouteHandler.qll b/javascript/ql/test/library-tests/frameworks/vercel/RouteHandler.qll new file mode 100644 index 000000000000..a6ca13ec1e9a --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/RouteHandler.qll @@ -0,0 +1,3 @@ +import javascript + +query predicate test_RouteHandler(VercelNode::RouteHandler rh) { any() } diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts new file mode 100644 index 000000000000..086673306b11 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts @@ -0,0 +1,8 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +// A default-exported function that has VercelRequest/VercelResponse at +// positions 1 and 2, not 0 and 1. Vercel does not invoke it this way, +// so it must NOT be recognised as a route handler. +export default function notAHandler(ctx: unknown, req: VercelRequest, res: VercelResponse) { + res.send(req.query.name); +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts new file mode 100644 index 000000000000..62ff97447724 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts @@ -0,0 +1,27 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +// A private helper with the same signature. Must NOT be recognised as a +// route handler, since Vercel only invokes the default export. +function internalHelper(req: VercelRequest, res: VercelResponse) { + res.send(req.query.name); +} + +export default function handler(req: VercelRequest, res: VercelResponse) { + // Request inputs + const q = req.query; // source: parameter + const b = req.body; // source: body + const c = req.cookies; // source: cookie + const u = req.url; // source: url (inherited from IncomingMessage) + const host = req.headers.host; // source: header (named) + const ref = req.headers.referer; // source: header (named) + + // Response header definition + res.setHeader("Content-Type", "text/html"); + + // Response send (direct and chained) + res.send(q); + res.status(200).send(b); + + // Redirect + res.redirect(req.query.url as string); +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/tests.expected b/javascript/ql/test/library-tests/frameworks/vercel/tests.expected new file mode 100644 index 000000000000..886ba9c5997c --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/tests.expected @@ -0,0 +1,22 @@ +test_RouteHandler +| src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +test_RequestSource +| src/vercel.ts:9:33:9:35 | req | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +test_ResponseSource +| src/vercel.ts:9:53:9:55 | res | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:23:3:23:17 | res.status(200) | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +test_HeaderDefinition +| src/vercel.ts:19:3:19:44 | res.set ... /html") | content-type | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +test_RedirectInvocation +| src/vercel.ts:26:3:26:39 | res.red ... string) | src/vercel.ts:26:16:26:38 | req.que ... string | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +test_RequestInputAccess +| src/vercel.ts:11:13:11:21 | req.query | parameter | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:12:13:12:20 | req.body | body | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:13:13:13:23 | req.cookies | cookie | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:14:13:14:19 | req.url | url | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:15:16:15:31 | req.headers.host | header | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:16:15:16:33 | req.headers.referer | header | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:26:16:26:24 | req.query | parameter | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +test_ResponseSendArgument +| src/vercel.ts:22:12:22:12 | q | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:23:24:23:24 | b | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | diff --git a/javascript/ql/test/library-tests/frameworks/vercel/tests.ql b/javascript/ql/test/library-tests/frameworks/vercel/tests.ql new file mode 100644 index 000000000000..da4f5ff6c735 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/tests.ql @@ -0,0 +1,7 @@ +import RouteHandler +import RequestSource +import ResponseSource +import RequestInputAccess +import HeaderDefinition +import ResponseSendArgument +import RedirectInvocation diff --git a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/CommandInjection.expected b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/CommandInjection.expected index f1d547bdfb1c..ddebc6baeaf3 100644 --- a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/CommandInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/CommandInjection.expected @@ -110,6 +110,8 @@ | promisification.js:151:28:151:31 | code | promisification.js:141:18:141:25 | req.body | promisification.js:151:28:151:31 | code | This command line depends on a $@. | promisification.js:141:18:141:25 | req.body | user-provided value | | promisification.js:152:25:152:28 | code | promisification.js:141:18:141:25 | req.body | promisification.js:152:25:152:28 | code | This command line depends on a $@. | promisification.js:141:18:141:25 | req.body | user-provided value | | third-party-command-injection.js:6:21:6:27 | command | third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command | This command line depends on a $@. | third-party-command-injection.js:5:20:5:26 | command | user-provided value | +| vercel.ts:6:8:6:21 | "echo " + name | vercel.ts:5:16:5:24 | req.query | vercel.ts:6:8:6:21 | "echo " + name | This command line depends on a $@. | vercel.ts:5:16:5:24 | req.query | user-provided value | +| vercel.ts:6:8:6:21 | "echo " + name | vercel.ts:5:16:5:29 | req.query.name | vercel.ts:6:8:6:21 | "echo " + name | This command line depends on a $@. | vercel.ts:5:16:5:29 | req.query.name | user-provided value | edges | actions.js:8:9:8:13 | title | actions.js:9:16:9:20 | title | provenance | | | actions.js:8:17:8:57 | github. ... t.title | actions.js:8:9:8:13 | title | provenance | | @@ -340,6 +342,10 @@ edges | promisification.js:141:11:141:14 | code | promisification.js:152:25:152:28 | code | provenance | | | promisification.js:141:18:141:25 | req.body | promisification.js:141:11:141:14 | code | provenance | | | third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command | provenance | | +| vercel.ts:5:9:5:12 | name | vercel.ts:6:18:6:21 | name | provenance | | +| vercel.ts:5:16:5:24 | req.query | vercel.ts:5:9:5:12 | name | provenance | | +| vercel.ts:5:16:5:29 | req.query.name | vercel.ts:5:9:5:12 | name | provenance | | +| vercel.ts:6:18:6:21 | name | vercel.ts:6:8:6:21 | "echo " + name | provenance | | nodes | actions.js:8:9:8:13 | title | semmle.label | title | | actions.js:8:17:8:57 | github. ... t.title | semmle.label | github. ... t.title | @@ -591,6 +597,11 @@ nodes | promisification.js:152:25:152:28 | code | semmle.label | code | | third-party-command-injection.js:5:20:5:26 | command | semmle.label | command | | third-party-command-injection.js:6:21:6:27 | command | semmle.label | command | +| vercel.ts:5:9:5:12 | name | semmle.label | name | +| vercel.ts:5:16:5:24 | req.query | semmle.label | req.query | +| vercel.ts:5:16:5:29 | req.query.name | semmle.label | req.query.name | +| vercel.ts:6:8:6:21 | "echo " + name | semmle.label | "echo " + name | +| vercel.ts:6:18:6:21 | name | semmle.label | name | subpaths | promisification.js:116:32:116:34 | cmd | promisification.js:118:21:118:23 | cmd | promisification.js:117:29:117:35 | resolve [Return] [resolve-value] | promisification.js:117:16:119:10 | new Pro ... }) [PromiseValue] | | promisification.js:122:42:122:45 | code | promisification.js:116:32:116:34 | cmd | promisification.js:117:16:119:10 | new Pro ... }) [PromiseValue] | promisification.js:122:24:122:46 | createE ... e(code) [PromiseValue] | diff --git a/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/vercel.ts b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/vercel.ts new file mode 100644 index 000000000000..73754cb88181 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-078/CommandInjection/vercel.ts @@ -0,0 +1,9 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; +import { exec } from "child_process"; + +export default function handler(req: VercelRequest, res: VercelResponse) { + const name = req.query.name as string; // $ Source + exec("echo " + name, (err, stdout) => { // $ Alert + res.send(stdout); + }); +} diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected index bb92027f9f75..8ee7067977cd 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected @@ -74,6 +74,8 @@ | tst2.js:113:12:113:17 | unsafe | tst2.js:105:9:105:9 | p | tst2.js:113:12:113:17 | unsafe | Cross-site scripting vulnerability due to a $@. | tst2.js:105:9:105:9 | p | user-provided value | | tst3.js:6:12:6:12 | p | tst3.js:5:9:5:9 | p | tst3.js:6:12:6:12 | p | Cross-site scripting vulnerability due to a $@. | tst3.js:5:9:5:9 | p | user-provided value | | tst3.js:12:12:12:15 | code | tst3.js:11:32:11:39 | reg.body | tst3.js:12:12:12:15 | code | Cross-site scripting vulnerability due to a $@. | tst3.js:11:32:11:39 | reg.body | user-provided value | +| vercel.ts:5:24:5:51 | `

${ ... }

` | vercel.ts:5:31:5:39 | req.query | vercel.ts:5:24:5:51 | `

${ ... }

` | Cross-site scripting vulnerability due to a $@. | vercel.ts:5:31:5:39 | req.query | user-provided value | +| vercel.ts:5:24:5:51 | `

${ ... }

` | vercel.ts:5:31:5:44 | req.query.name | vercel.ts:5:24:5:51 | `

${ ... }

` | Cross-site scripting vulnerability due to a $@. | vercel.ts:5:31:5:44 | req.query.name | user-provided value | edges | ReflectedXss.js:7:33:7:45 | req.params.id | ReflectedXss.js:7:14:7:45 | "Unknow ... rams.id | provenance | | | ReflectedXss.js:16:31:16:39 | params.id | ReflectedXss.js:16:12:16:39 | "Unknow ... rams.id | provenance | | @@ -259,6 +261,8 @@ edges | tst3.js:11:9:11:12 | code | tst3.js:12:12:12:15 | code | provenance | | | tst3.js:11:16:11:74 | prettie ... bel" }) | tst3.js:11:9:11:12 | code | provenance | | | tst3.js:11:32:11:39 | reg.body | tst3.js:11:16:11:74 | prettie ... bel" }) | provenance | | +| vercel.ts:5:31:5:39 | req.query | vercel.ts:5:24:5:51 | `

${ ... }

` | provenance | | +| vercel.ts:5:31:5:44 | req.query.name | vercel.ts:5:24:5:51 | `

${ ... }

` | provenance | | nodes | ReflectedXss.js:7:14:7:45 | "Unknow ... rams.id | semmle.label | "Unknow ... rams.id | | ReflectedXss.js:7:33:7:45 | req.params.id | semmle.label | req.params.id | @@ -497,5 +501,8 @@ nodes | tst3.js:11:16:11:74 | prettie ... bel" }) | semmle.label | prettie ... bel" }) | | tst3.js:11:32:11:39 | reg.body | semmle.label | reg.body | | tst3.js:12:12:12:15 | code | semmle.label | code | +| vercel.ts:5:24:5:51 | `

${ ... }

` | semmle.label | `

${ ... }

` | +| vercel.ts:5:31:5:39 | req.query | semmle.label | req.query | +| vercel.ts:5:31:5:44 | req.query.name | semmle.label | req.query.name | subpaths | ReflectedXssGood3.js:139:24:139:26 | url | ReflectedXssGood3.js:68:22:68:26 | value | ReflectedXssGood3.js:108:10:108:23 | parts.join('') | ReflectedXssGood3.js:139:12:139:27 | escapeHtml3(url) | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected index fa2886fb0cd3..a538fcd8ee70 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected @@ -72,3 +72,5 @@ | tst2.js:113:12:113:17 | unsafe | Cross-site scripting vulnerability due to $@. | tst2.js:105:9:105:9 | p | user-provided value | | tst3.js:6:12:6:12 | p | Cross-site scripting vulnerability due to $@. | tst3.js:5:9:5:9 | p | user-provided value | | tst3.js:12:12:12:15 | code | Cross-site scripting vulnerability due to $@. | tst3.js:11:32:11:39 | reg.body | user-provided value | +| vercel.ts:5:24:5:51 | `

${ ... }

` | Cross-site scripting vulnerability due to $@. | vercel.ts:5:31:5:39 | req.query | user-provided value | +| vercel.ts:5:24:5:51 | `

${ ... }

` | Cross-site scripting vulnerability due to $@. | vercel.ts:5:31:5:44 | req.query.name | user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/vercel.ts b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/vercel.ts new file mode 100644 index 000000000000..dbd90171444d --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/vercel.ts @@ -0,0 +1,6 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +export default function handler(req: VercelRequest, res: VercelResponse) { + res.setHeader("Content-Type", "text/html"); + res.status(200).send(`

${req.query.name}

`); // $ Alert +} diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected b/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected index f28fb93238d9..17d280d38096 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/SqlInjection.expected @@ -156,6 +156,8 @@ | tst3.js:9:14:9:19 | query1 | tst3.js:8:16:8:34 | req.params.category | tst3.js:9:14:9:19 | query1 | This query string depends on a $@. | tst3.js:8:16:8:34 | req.params.category | user-provided value | | tst4.js:8:10:8:66 | 'SELECT ... d + '"' | tst4.js:8:46:8:60 | $routeParams.id | tst4.js:8:10:8:66 | 'SELECT ... d + '"' | This query string depends on a $@. | tst4.js:8:46:8:60 | $routeParams.id | user-provided value | | tst.js:10:10:10:64 | 'SELECT ... d + '"' | tst.js:10:46:10:58 | req.params.id | tst.js:10:10:10:64 | 'SELECT ... d + '"' | This query string depends on a $@. | tst.js:10:46:10:58 | req.params.id | user-provided value | +| vercel.ts:7:14:7:51 | "SELECT ... " + id | vercel.ts:6:14:6:22 | req.query | vercel.ts:7:14:7:51 | "SELECT ... " + id | This query string depends on a $@. | vercel.ts:6:14:6:22 | req.query | user-provided value | +| vercel.ts:7:14:7:51 | "SELECT ... " + id | vercel.ts:6:14:6:25 | req.query.id | vercel.ts:7:14:7:51 | "SELECT ... " + id | This query string depends on a $@. | vercel.ts:6:14:6:25 | req.query.id | user-provided value | edges | athena.js:9:11:9:19 | userQuery | athena.js:14:30:14:38 | userQuery | provenance | | | athena.js:9:11:9:19 | userQuery | athena.js:24:22:24:30 | userQuery | provenance | | @@ -620,6 +622,10 @@ edges | tst3.js:8:16:8:34 | req.params.category | tst3.js:7:7:7:12 | query1 | provenance | | | tst4.js:8:46:8:60 | $routeParams.id | tst4.js:8:10:8:66 | 'SELECT ... d + '"' | provenance | | | tst.js:10:46:10:58 | req.params.id | tst.js:10:10:10:64 | 'SELECT ... d + '"' | provenance | | +| vercel.ts:6:9:6:10 | id | vercel.ts:7:50:7:51 | id | provenance | | +| vercel.ts:6:14:6:22 | req.query | vercel.ts:6:9:6:10 | id | provenance | | +| vercel.ts:6:14:6:25 | req.query.id | vercel.ts:6:9:6:10 | id | provenance | | +| vercel.ts:7:50:7:51 | id | vercel.ts:7:14:7:51 | "SELECT ... " + id | provenance | | nodes | athena.js:9:11:9:19 | userQuery | semmle.label | userQuery | | athena.js:9:23:9:30 | req.body | semmle.label | req.body | @@ -1029,4 +1035,9 @@ nodes | tst4.js:8:46:8:60 | $routeParams.id | semmle.label | $routeParams.id | | tst.js:10:10:10:64 | 'SELECT ... d + '"' | semmle.label | 'SELECT ... d + '"' | | tst.js:10:46:10:58 | req.params.id | semmle.label | req.params.id | +| vercel.ts:6:9:6:10 | id | semmle.label | id | +| vercel.ts:6:14:6:22 | req.query | semmle.label | req.query | +| vercel.ts:6:14:6:25 | req.query.id | semmle.label | req.query.id | +| vercel.ts:7:14:7:51 | "SELECT ... " + id | semmle.label | "SELECT ... " + id | +| vercel.ts:7:50:7:51 | id | semmle.label | id | subpaths diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/vercel.ts b/javascript/ql/test/query-tests/Security/CWE-089/untyped/vercel.ts new file mode 100644 index 000000000000..b511f107747f --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/vercel.ts @@ -0,0 +1,10 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; +const mysql = require("mysql"); +const conn = mysql.createConnection({}); + +export default function handler(req: VercelRequest, res: VercelResponse) { + const id = req.query.id as string; // $ Source + conn.query("SELECT * FROM users WHERE id = " + id, (err: any, rows: any) => { // $ Alert + res.json(rows); + }); +} 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 79383f585215..b3d939a30c5f 100644 --- a/javascript/ql/test/query-tests/Security/CWE-918/RequestForgery.expected +++ b/javascript/ql/test/query-tests/Security/CWE-918/RequestForgery.expected @@ -40,6 +40,8 @@ | serverSide.js:145:5:145:25 | axios.g ... dedUrl) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:145:15:145:24 | encodedUrl | The $@ of this request depends on a $@. | serverSide.js:145:15:145:24 | encodedUrl | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value | | serverSide.js:147:5:147:25 | axios.g ... pedUrl) | serverSide.js:139:19:139:31 | req.query.url | serverSide.js:147:15:147:24 | escapedUrl | The $@ of this request depends on a $@. | serverSide.js:147:15:147:24 | escapedUrl | URL | serverSide.js:139:19:139:31 | req.query.url | user-provided value | | serverSide.js:151:1:151:15 | request(custom) | serverSide.js:150:16:150:51 | require ... ource() | serverSide.js:151:9:151:14 | custom | The $@ of this request depends on a $@. | serverSide.js:151:9:151:14 | custom | URL | serverSide.js:150:16:150:51 | require ... ource() | user-provided value | +| vercel.ts:5:26:5:35 | fetch(url) | vercel.ts:4:15:4:23 | req.query | vercel.ts:5:32:5:34 | url | The $@ of this request depends on a $@. | vercel.ts:5:32:5:34 | url | URL | vercel.ts:4:15:4:23 | req.query | user-provided value | +| vercel.ts:5:26:5:35 | fetch(url) | vercel.ts:4:15:4:27 | req.query.url | vercel.ts:5:32:5:34 | url | The $@ of this request depends on a $@. | vercel.ts:5:32:5:34 | url | URL | vercel.ts:4:15:4:27 | req.query.url | user-provided value | edges | Request/app/api/proxy/route2.serverSide.ts:4:9:4:15 | { url } | Request/app/api/proxy/route2.serverSide.ts:4:11:4:13 | url | provenance | | | Request/app/api/proxy/route2.serverSide.ts:4:11:4:13 | url | Request/app/api/proxy/route2.serverSide.ts:5:27:5:29 | url | provenance | | @@ -147,6 +149,9 @@ edges | serverSide.js:146:31:146:35 | input | serverSide.js:146:24:146:36 | escape(input) | provenance | | | serverSide.js:150:7:150:12 | custom | serverSide.js:151:9:151:14 | custom | provenance | | | serverSide.js:150:16:150:51 | require ... ource() | serverSide.js:150:7:150:12 | custom | provenance | | +| vercel.ts:4:9:4:11 | url | vercel.ts:5:32:5:34 | url | provenance | | +| vercel.ts:4:15:4:23 | req.query | vercel.ts:4:9:4:11 | url | provenance | | +| vercel.ts:4:15:4:27 | req.query.url | vercel.ts:4:9:4:11 | url | provenance | | nodes | Request/app/api/proxy/route2.serverSide.ts:4:9:4:15 | { url } | semmle.label | { url } | | Request/app/api/proxy/route2.serverSide.ts:4:11:4:13 | url | semmle.label | url | @@ -277,4 +282,8 @@ nodes | serverSide.js:150:7:150:12 | custom | semmle.label | custom | | serverSide.js:150:16:150:51 | require ... ource() | semmle.label | require ... ource() | | serverSide.js:151:9:151:14 | custom | semmle.label | custom | +| vercel.ts:4:9:4:11 | url | semmle.label | url | +| vercel.ts:4:15:4:23 | req.query | semmle.label | req.query | +| vercel.ts:4:15:4:27 | req.query.url | semmle.label | req.query.url | +| vercel.ts:5:32:5:34 | url | semmle.label | url | subpaths diff --git a/javascript/ql/test/query-tests/Security/CWE-918/vercel.ts b/javascript/ql/test/query-tests/Security/CWE-918/vercel.ts new file mode 100644 index 000000000000..e383088489dc --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-918/vercel.ts @@ -0,0 +1,7 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + const url = req.query.url as string; // $ Source[js/request-forgery] + const response = await fetch(url); // $ Alert[js/request-forgery] + res.json(await response.json()); +} From cff07342f5cbd5a3658be054e764b79ddac21bd4 Mon Sep 17 00:00:00 2001 From: murderteeth Date: Mon, 13 Apr 2026 17:31:29 +0000 Subject: [PATCH 2/6] Recognize legacy @now/node type aliases Extends the Vercel serverless handler detection to also match the deprecated Zeit-era @now/node package with NowRequest/NowResponse types. Per-review feedback from asgerf, these aliases still appear in real-world code. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ql/lib/semmle/javascript/frameworks/VercelNode.qll | 4 ++-- .../ql/test/library-tests/frameworks/vercel/src/now.ts | 7 +++++++ .../ql/test/library-tests/frameworks/vercel/tests.expected | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/vercel/src/now.ts diff --git a/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll b/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll index 233f103825c6..e31d40dcf9a6 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll @@ -32,8 +32,8 @@ module VercelNode { this = any(Module m).getAnExportedValue("default").getAFunctionValue() and req = this.getParameter(0) and res = this.getParameter(1) and - req.hasUnderlyingType("@vercel/node", "VercelRequest") and - res.hasUnderlyingType("@vercel/node", "VercelResponse") + req.hasUnderlyingType(["@vercel/node", "@now/node"], ["NowRequest", "VercelRequest"]) and + res.hasUnderlyingType(["@vercel/node", "@now/node"], ["NowResponse", "VercelResponse"]) } /** Gets the parameter that contains the request object. */ diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts new file mode 100644 index 000000000000..a8ac7020408c --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts @@ -0,0 +1,7 @@ +import type { NowRequest, NowResponse } from "@now/node"; + +// Legacy Zeit-era aliases. The model should treat these identically to +// the modern @vercel/node NowRequest -> VercelRequest, NowResponse -> VercelResponse. +export default function handler(req: NowRequest, res: NowResponse) { + res.send(req.query.name); +} diff --git a/javascript/ql/test/library-tests/frameworks/vercel/tests.expected b/javascript/ql/test/library-tests/frameworks/vercel/tests.expected index 886ba9c5997c..a2929999f235 100644 --- a/javascript/ql/test/library-tests/frameworks/vercel/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/vercel/tests.expected @@ -1,8 +1,11 @@ test_RouteHandler +| src/now.ts:5:16:7:1 | functio ... ame);\\n} | | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | test_RequestSource +| src/now.ts:5:33:5:35 | req | src/now.ts:5:16:7:1 | functio ... ame);\\n} | | src/vercel.ts:9:33:9:35 | req | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | test_ResponseSource +| src/now.ts:5:50:5:52 | res | src/now.ts:5:16:7:1 | functio ... ame);\\n} | | src/vercel.ts:9:53:9:55 | res | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | | src/vercel.ts:23:3:23:17 | res.status(200) | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | test_HeaderDefinition @@ -10,6 +13,7 @@ test_HeaderDefinition test_RedirectInvocation | src/vercel.ts:26:3:26:39 | res.red ... string) | src/vercel.ts:26:16:26:38 | req.que ... string | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | test_RequestInputAccess +| src/now.ts:6:12:6:20 | req.query | parameter | src/now.ts:5:16:7:1 | functio ... ame);\\n} | | src/vercel.ts:11:13:11:21 | req.query | parameter | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | | src/vercel.ts:12:13:12:20 | req.body | body | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | | src/vercel.ts:13:13:13:23 | req.cookies | cookie | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | @@ -18,5 +22,6 @@ test_RequestInputAccess | src/vercel.ts:16:15:16:33 | req.headers.referer | header | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | | src/vercel.ts:26:16:26:24 | req.query | parameter | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | test_ResponseSendArgument +| src/now.ts:6:12:6:25 | req.query.name | src/now.ts:5:16:7:1 | functio ... ame);\\n} | | src/vercel.ts:22:12:22:12 | q | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | | src/vercel.ts:23:24:23:24 | b | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | From 47915328e6c91cfd3dfe411d83b7e44ba44d7e02 Mon Sep 17 00:00:00 2001 From: murderteeth Date: Mon, 13 Apr 2026 17:35:08 +0000 Subject: [PATCH 3/6] Address Copilot review nits Fixes US spelling (recognised -> recognized) across docs, QLDoc, change note, and test fixture comments. Clarifies the handler QLDoc to note sync/async support. Renames the supported-frameworks entry from "vercel" to "Vercel (@vercel/node)" to avoid implying broader platform coverage. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/codeql/reusables/supported-frameworks.rst | 2 +- javascript/ql/lib/change-notes/2026-04-12-vercel-node.md | 2 +- .../ql/lib/semmle/javascript/frameworks/VercelNode.qll | 9 +++++---- .../library-tests/frameworks/vercel/src/notahandler.ts | 2 +- .../ql/test/library-tests/frameworks/vercel/src/now.ts | 2 +- .../test/library-tests/frameworks/vercel/src/vercel.ts | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/codeql/reusables/supported-frameworks.rst b/docs/codeql/reusables/supported-frameworks.rst index 581f34f92271..930cdc6b629a 100644 --- a/docs/codeql/reusables/supported-frameworks.rst +++ b/docs/codeql/reusables/supported-frameworks.rst @@ -197,7 +197,7 @@ and the CodeQL library pack ``codeql/javascript-all`` (`changelog void`, where - * the types are imported from the `@vercel/node` package. The Vercel runtime - * invokes the default export for every incoming HTTP request. + * taking parameters `(req: VercelRequest, res: VercelResponse)`, where the + * types are imported from the `@vercel/node` package. The default export may + * be synchronous or `async`, and the Vercel runtime invokes it for every + * incoming HTTP request. */ module VercelNode { /** @@ -20,7 +21,7 @@ module VercelNode { * `VercelResponse` from `@vercel/node`. * * Since `@vercel/node` is commonly imported as a type-only import, handlers - * are recognised by their TypeScript parameter types. The default-export + * are recognized by their TypeScript parameter types. The default-export * constraint excludes private helpers or test utilities that share the * same signature. */ diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts index 086673306b11..7bb1d903a84c 100644 --- a/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/notahandler.ts @@ -2,7 +2,7 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; // A default-exported function that has VercelRequest/VercelResponse at // positions 1 and 2, not 0 and 1. Vercel does not invoke it this way, -// so it must NOT be recognised as a route handler. +// so it must NOT be recognized as a route handler. export default function notAHandler(ctx: unknown, req: VercelRequest, res: VercelResponse) { res.send(req.query.name); } diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts index a8ac7020408c..33a34d47e2a9 100644 --- a/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/now.ts @@ -1,7 +1,7 @@ import type { NowRequest, NowResponse } from "@now/node"; // Legacy Zeit-era aliases. The model should treat these identically to -// the modern @vercel/node NowRequest -> VercelRequest, NowResponse -> VercelResponse. +// the modern @vercel/node types (NowRequest -> VercelRequest, NowResponse -> VercelResponse). export default function handler(req: NowRequest, res: NowResponse) { res.send(req.query.name); } diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts index 62ff97447724..0dae664e2c44 100644 --- a/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts @@ -1,6 +1,6 @@ import type { VercelRequest, VercelResponse } from "@vercel/node"; -// A private helper with the same signature. Must NOT be recognised as a +// A private helper with the same signature. Must NOT be recognized as a // route handler, since Vercel only invokes the default export. function internalHelper(req: VercelRequest, res: VercelResponse) { res.send(req.query.name); From f15d53f3b9e0131885cefdd1c12d9dd5a160ea86 Mon Sep 17 00:00:00 2001 From: murderteeth <89237203+murderteeth@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:19:01 -0400 Subject: [PATCH 4/6] Update javascript/ql/lib/change-notes/2026-04-12-vercel-node.md Co-authored-by: Asger F --- javascript/ql/lib/change-notes/2026-04-12-vercel-node.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/ql/lib/change-notes/2026-04-12-vercel-node.md b/javascript/ql/lib/change-notes/2026-04-12-vercel-node.md index b248a4b3c45e..39802258b02f 100644 --- a/javascript/ql/lib/change-notes/2026-04-12-vercel-node.md +++ b/javascript/ql/lib/change-notes/2026-04-12-vercel-node.md @@ -1,4 +1,4 @@ --- -category: newFeature +category: feature --- * Added support for [`@vercel/node`](https://www.npmjs.com/package/@vercel/node) Vercel serverless functions. Handlers are recognized via the `VercelRequest`/`VercelResponse` TypeScript parameter types, and standard security queries (`js/reflected-xss`, `js/request-forgery`, `js/sql-injection`, `js/command-line-injection`, etc.) now detect vulnerabilities in Vercel API route files. From 1b87140ce7def7442f46cd11ee5fc09483aba5d3 Mon Sep 17 00:00:00 2001 From: murderteeth <89237203+murderteeth@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:57:06 +0000 Subject: [PATCH 5/6] Regenerate DatabaseAccesses.expected for new vercel.ts fixture The CWE-089/untyped/vercel.ts fixture added in this PR introduces a conn.query(...) call that DatabaseAccesses.ql reports, so its .expected baseline needs the corresponding entry. Output produced by `codeql test accept`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Security/CWE-089/untyped/DatabaseAccesses.expected | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected b/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected index 52ce3076ba70..4a4fe13b3231 100644 --- a/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected +++ b/javascript/ql/test/query-tests/Security/CWE-089/untyped/DatabaseAccesses.expected @@ -93,3 +93,4 @@ | tst3.js:16:3:18:4 | pool.qu ... ts\\n }) | | tst4.js:8:3:8:67 | db.get( ... + '"') | | tst.js:10:3:10:65 | db.get( ... + '"') | +| vercel.ts:7:3:9:4 | conn.qu ... );\\n }) | From 18b06f1cf465962d70915378e36188747c5f748c Mon Sep 17 00:00:00 2001 From: murderteeth <89237203+murderteeth@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:14:53 +0000 Subject: [PATCH 6/6] Model res.json and res.jsonp as Vercel response sinks Vercel API handlers more often return JSON than HTML, so res.send is not the only response body sink that matters. Mirror Express's ResponseJsonCall by also matching res.json(...) and res.jsonp(...) on the response (direct and chained), and exercise the new behavior in the library-test fixture. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../javascript/frameworks/VercelNode.qll | 6 ++-- .../frameworks/vercel/src/vercel.ts | 5 +++ .../frameworks/vercel/tests.expected | 34 +++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll b/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll index ae206c9f9151..9dcb25cf5db6 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll @@ -141,8 +141,8 @@ module VercelNode { } /** - * An argument to `res.send(...)` on a Vercel response, including chained - * calls such as `res.status(200).send(...)`. + * An argument to `res.send(...)`, `res.json(...)`, or `res.jsonp(...)` on a + * Vercel response, including chained calls such as `res.status(200).json(...)`. */ private class ResponseSendArgument extends Http::ResponseSendArgument { RouteHandler rh; @@ -150,7 +150,7 @@ module VercelNode { ResponseSendArgument() { exists(Http::Servers::ResponseSource src | (src instanceof ResponseSource or src instanceof ChainedResponseSource) and - this = src.ref().getAMethodCall("send").getArgument(0) and + this = src.ref().getAMethodCall(["send", "json", "jsonp"]).getArgument(0) and rh = src.getRouteHandler() ) } diff --git a/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts b/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts index 0dae664e2c44..23956251ef45 100644 --- a/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts +++ b/javascript/ql/test/library-tests/frameworks/vercel/src/vercel.ts @@ -22,6 +22,11 @@ export default function handler(req: VercelRequest, res: VercelResponse) { res.send(q); res.status(200).send(b); + // JSON response (direct and chained) + res.json(c); + res.status(200).json(u); + res.jsonp(host); + // Redirect res.redirect(req.query.url as string); } diff --git a/javascript/ql/test/library-tests/frameworks/vercel/tests.expected b/javascript/ql/test/library-tests/frameworks/vercel/tests.expected index a2929999f235..92d309cc02fe 100644 --- a/javascript/ql/test/library-tests/frameworks/vercel/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/vercel/tests.expected @@ -1,27 +1,31 @@ test_RouteHandler | src/now.ts:5:16:7:1 | functio ... ame);\\n} | -| src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | test_RequestSource | src/now.ts:5:33:5:35 | req | src/now.ts:5:16:7:1 | functio ... ame);\\n} | -| src/vercel.ts:9:33:9:35 | req | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:9:33:9:35 | req | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | test_ResponseSource | src/now.ts:5:50:5:52 | res | src/now.ts:5:16:7:1 | functio ... ame);\\n} | -| src/vercel.ts:9:53:9:55 | res | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:23:3:23:17 | res.status(200) | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:9:53:9:55 | res | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:23:3:23:17 | res.status(200) | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:27:3:27:17 | res.status(200) | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | test_HeaderDefinition -| src/vercel.ts:19:3:19:44 | res.set ... /html") | content-type | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:19:3:19:44 | res.set ... /html") | content-type | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | test_RedirectInvocation -| src/vercel.ts:26:3:26:39 | res.red ... string) | src/vercel.ts:26:16:26:38 | req.que ... string | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:31:3:31:39 | res.red ... string) | src/vercel.ts:31:16:31:38 | req.que ... string | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | test_RequestInputAccess | src/now.ts:6:12:6:20 | req.query | parameter | src/now.ts:5:16:7:1 | functio ... ame);\\n} | -| src/vercel.ts:11:13:11:21 | req.query | parameter | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:12:13:12:20 | req.body | body | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:13:13:13:23 | req.cookies | cookie | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:14:13:14:19 | req.url | url | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:15:16:15:31 | req.headers.host | header | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:16:15:16:33 | req.headers.referer | header | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:26:16:26:24 | req.query | parameter | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:11:13:11:21 | req.query | parameter | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:12:13:12:20 | req.body | body | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:13:13:13:23 | req.cookies | cookie | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:14:13:14:19 | req.url | url | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:15:16:15:31 | req.headers.host | header | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:16:15:16:33 | req.headers.referer | header | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:31:16:31:24 | req.query | parameter | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | test_ResponseSendArgument | src/now.ts:6:12:6:25 | req.query.name | src/now.ts:5:16:7:1 | functio ... ame);\\n} | -| src/vercel.ts:22:12:22:12 | q | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | -| src/vercel.ts:23:24:23:24 | b | src/vercel.ts:9:16:27:1 | functio ... ing);\\n} | +| src/vercel.ts:22:12:22:12 | q | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:23:24:23:24 | b | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:26:12:26:12 | c | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:27:24:27:24 | u | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} | +| src/vercel.ts:28:13:28:16 | host | src/vercel.ts:9:16:32:1 | functio ... ing);\\n} |