Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 (@vercel/node), Serverless framework
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 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.
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
201 changes: 201 additions & 0 deletions javascript/ql/lib/semmle/javascript/frameworks/VercelNode.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* 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
* 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 {
/**
* 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 recognized 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", "@now/node"], ["NowRequest", "VercelRequest"]) and
res.hasUnderlyingType(["@vercel/node", "@now/node"], ["NowResponse", "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) }
}
}
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 recognized as a route handler.
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,7 @@
import type { NowRequest, NowResponse } from "@now/node";

// Legacy Zeit-era aliases. The model should treat these identically to
// the modern @vercel/node types (NowRequest -> VercelRequest, NowResponse -> VercelResponse).
export default function handler(req: NowRequest, res: NowResponse) {
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 recognized 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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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
| 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/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} |
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} |
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