Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Comment thread
murderteeth marked this conversation as resolved.
Outdated
---
* 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