Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/codeql/reusables/supported-frameworks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ and the CodeQL library pack ``codeql/javascript-all`` (`changelog <https://githu
superagent, Network communicator
swig, templating language
underscore, Utility library
vercel, Serverless framework
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row says “vercel”, but the added modeling is specifically for @vercel/node serverless functions. Consider renaming the entry to be more specific (for example, “Vercel (@vercel/node)” or similar) to avoid implying broader Vercel platform coverage.

Suggested change
vercel, Serverless framework
Vercel (@vercel/node), Serverless framework

Copilot uses AI. Check for mistakes.
vue, HTML framework


Expand Down
4 changes: 4 additions & 0 deletions javascript/ql/lib/change-notes/2026-04-12-vercel-node.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: newFeature
---
* Added support for [`@vercel/node`](https://www.npmjs.com/package/@vercel/node) Vercel serverless functions. Handlers are recognised 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.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling: change “recognised” to “recognized” for consistency with other JavaScript change notes (which use US spelling).

Suggested change
* Added support for [`@vercel/node`](https://www.npmjs.com/package/@vercel/node) Vercel serverless functions. Handlers are recognised 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.
* 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.

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions javascript/ql/lib/javascript.qll
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import semmle.javascript.frameworks.TorrentLibraries
import semmle.javascript.frameworks.Typeahead
import semmle.javascript.frameworks.TrustedTypes
import semmle.javascript.frameworks.UriLibraries
import semmle.javascript.frameworks.VercelNode
import semmle.javascript.frameworks.Vue
import semmle.javascript.frameworks.Vuex
import semmle.javascript.frameworks.Webix
Expand Down
200 changes: 200 additions & 0 deletions javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* Provides classes for working with [@vercel/node](https://www.npmjs.com/package/@vercel/node) Vercel serverless functions.
*/

import javascript
import semmle.javascript.frameworks.HTTP

/**
* Provides classes for working with [@vercel/node](https://www.npmjs.com/package/@vercel/node) Vercel serverless functions.
*
* A Vercel serverless function is a module whose default export is a function
* with signature `(req: VercelRequest, res: VercelResponse) => void`, where
* the types are imported from the `@vercel/node` package. The Vercel runtime
* invokes the default export for every incoming HTTP request.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment says the handler signature returns void, but handlers can be async/return a Promise (and the PR’s own fixtures include an async function handler). Please update the comment to avoid implying a synchronous-only handler.

Suggested change
* with signature `(req: VercelRequest, res: VercelResponse) => 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.

Copilot uses AI. Check for mistakes.
*/
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
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling consistency: this file uses British spelling (“recognised”), but the surrounding JavaScript framework libraries consistently use US spelling (“recognized”). Please change to “recognized” here (and keep wording consistent across related docs/tests).

Suggested change
* are recognised by their TypeScript parameter types. The default-export
* are recognized by their TypeScript parameter types. The default-export

Copilot uses AI. Check for mistakes.
* 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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"])

After testing this on some code in the wild, it seems there are some deprecated aliases for these types worth covering here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the fast review! your request is addressed here cff0734

}

/** 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) }
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import javascript

query predicate test_RequestSource(Http::Servers::RequestSource src, VercelNode::RouteHandler rh) {
src.getRouteHandler() = rh
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import javascript

query predicate test_ResponseSendArgument(
Http::ResponseSendArgument arg, VercelNode::RouteHandler rh
) {
arg.getRouteHandler() = rh
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import javascript

query predicate test_ResponseSource(Http::Servers::ResponseSource src, VercelNode::RouteHandler rh) {
src.getRouteHandler() = rh
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import javascript

query predicate test_RouteHandler(VercelNode::RouteHandler rh) { any() }
Original file line number Diff line number Diff line change
@@ -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.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling in comment: change “recognised” to “recognized” to match the rest of the JavaScript codebase’s comment spelling.

Suggested change
// so it must NOT be recognised as a route handler.
// so it must NOT be recognized as a route handler.

Copilot uses AI. Check for mistakes.
export default function notAHandler(ctx: unknown, req: VercelRequest, res: VercelResponse) {
res.send(req.query.name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { VercelRequest, VercelResponse } from "@vercel/node";

// A private helper with the same signature. Must NOT be recognised as a
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling in comment: change “recognised” to “recognized” to match the rest of the JavaScript codebase’s comment spelling.

Suggested change
// 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

Copilot uses AI. Check for mistakes.
// 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);
}
Original file line number Diff line number Diff line change
@@ -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} |
7 changes: 7 additions & 0 deletions javascript/ql/test/library-tests/frameworks/vercel/tests.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import RouteHandler
import RequestSource
import ResponseSource
import RequestInputAccess
import HeaderDefinition
import ResponseSendArgument
import RedirectInvocation
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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] |
Original file line number Diff line number Diff line change
@@ -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);
});
}
Loading
Loading