Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/NodeJSLib.qll
Asger F c687dc93b0 JS: Add overlay[global] to abstract classes with fields
Some abstract classes defines fields without binding them, leaving it up to the subclasses to bind them. When combined with overlay[local?], the charpred for such an abstract class can become local, while the subclasses are global. The means the charpred needs to be materialized, even though it doesn't bind the fields, leading to a cartesian product.
2026-01-07 11:05:41 +01:00

1300 lines
42 KiB
Plaintext

/**
* Provides classes for modeling the Node.js standard library.
*/
import javascript
import semmle.javascript.frameworks.HTTP
import semmle.javascript.security.SensitiveActions
private import semmle.javascript.dataflow.internal.PreCallGraphStep
module NodeJSLib {
overlay[local?]
private GlobalVariable processVariable() { variables(result, "process", any(GlobalScope sc)) }
overlay[local?]
pragma[nomagic]
private GlobalVarAccess processExprInTopLevel(TopLevel tl) {
result = processVariable().getAnAccess() and
tl = result.getTopLevel()
}
overlay[local?]
pragma[nomagic]
private GlobalVarAccess processExprInNodeModule() {
result = processExprInTopLevel(any(NodeModule m))
}
/**
* An access to the global `process` variable in a Node.js module, interpreted as
* an import of the `process` module.
*/
overlay[local?]
private class ImplicitProcessImport extends DataFlow::ModuleImportNode::Range {
ImplicitProcessImport() { this = DataFlow::exprNode(processExprInNodeModule()) }
override string getPath() { result = "process" }
}
/**
* Gets a reference to the 'process' object.
*/
DataFlow::SourceNode process() {
result = DataFlow::globalVarRef("process") or
result = DataFlow::moduleImport("process")
}
/**
* Gets a reference to a member of the 'process' object.
*/
private DataFlow::SourceNode processMember(string member) {
result = process().getAPropertyRead(member)
}
/**
* Holds if `call` is an invocation of `http.createServer` or `https.createServer`.
*/
predicate isCreateServer(DataFlow::CallNode call) {
exists(string pkg, string fn |
pkg = "http" and fn = "createServer"
or
pkg = "https" and fn = "createServer"
or
// http2 compatibility API
pkg = "http2" and fn = "createServer"
or
pkg = "http2" and fn = "createSecureServer"
|
call = DataFlow::moduleMember(pkg, fn).getAnInvocation()
)
}
/**
* A Node.js HTTP response.
*
* A server library that provides an (enhanced) NodesJS HTTP response
* object should implement a library specific subclass of this class.
*/
abstract class ResponseNode extends Http::Servers::StandardResponseNode { }
/**
* A Node.js HTTP request.
*
* A server library that provides an (enhanced) NodesJS HTTP request
* object should implement a library specific subclass of this class.
*/
abstract class RequestNode extends Http::Servers::StandardRequestNode { }
/**
* A function used as an Node.js server route handler.
*
* By default, only handlers installed by an Node.js server route setup are recognized,
* but support for other kinds of route handlers can be added by implementing
* additional subclasses of this class.
*/
abstract class RouteHandler extends Http::Servers::StandardRouteHandler, DataFlow::FunctionNode {
/**
* Gets the parameter of the route handler that contains the request object.
*/
DataFlow::ParameterNode getRequestParameter() { result = this.getParameter(0) }
/**
* Gets the parameter of the route handler that contains the response object.
*/
DataFlow::ParameterNode getResponseParameter() { result = this.getParameter(1) }
}
/**
* A route handler installed by a route setup.
*/
class StandardRouteHandler extends RouteHandler {
StandardRouteHandler() { this = any(RouteSetup setup).getARouteHandler() }
}
/**
* A Node.js response source.
*/
abstract class ResponseSource extends Http::Servers::ResponseSource { }
/**
* A standard Node.js response source, that is, the response parameter of a
* route handler.
*/
private class StandardResponseSource extends ResponseSource {
RouteHandler rh;
StandardResponseSource() { this = rh.getResponseParameter() }
/**
* Gets the route handler that provides this response.
*/
override RouteHandler getRouteHandler() { result = rh }
}
/**
* A Node.js request source.
*/
abstract class RequestSource extends Http::Servers::RequestSource { }
/**
* A standard Node.js request source, that is, the request parameter of a
* route handler.
*/
private class StandardRequestSource extends RequestSource {
RouteHandler rh;
StandardRequestSource() { this = rh.getRequestParameter() }
/**
* Gets the route handler that handles this request.
*/
override RouteHandler getRouteHandler() { result = rh }
}
/**
* A builtin Node.js HTTP response.
*/
private class BuiltinRouteHandlerResponseNode extends ResponseNode {
BuiltinRouteHandlerResponseNode() { src instanceof ResponseSource }
}
/**
* A builtin Node.js HTTP request.
*/
private class BuiltinRouteHandlerRequestNode extends RequestNode {
BuiltinRouteHandlerRequestNode() { src instanceof RequestSource }
}
/**
* An access to a user-controlled Node.js request input.
*/
private class RequestInputAccess extends Http::RequestInputAccess {
RequestNode request;
string kind;
RequestInputAccess() {
// `req.url` / `req.body`
kind = ["url", "body"] and
this.(DataFlow::PropRead).accesses(request, kind)
or
exists(DataFlow::PropRead headers |
// `req.headers.cookie`
kind = "cookie" and
headers.accesses(request, "headers") and
this.(DataFlow::PropRead).accesses(headers, "cookie")
)
or
exists(RequestHeaderAccess access | this = access |
request = access.getRequest() and
kind = "header"
)
}
override Http::RouteHandler getRouteHandler() { result = request.getRouteHandler() }
override string getKind() { result = kind }
}
/**
* An access to an HTTP header (other than "Cookie") on an incoming Node.js request object.
*/
private class RequestHeaderAccess extends Http::RequestHeaderAccess {
RequestNode request;
RequestHeaderAccess() {
exists(DataFlow::PropRead headers, string name |
// `req.headers.<name>`
name != "cookie" and
headers.accesses(request, "headers") and
this.(DataFlow::PropRead).accesses(headers, name)
)
}
override string getAHeaderName() {
result = this.(DataFlow::PropRead).getPropertyName().toLowerCase()
}
override Http::RouteHandler getRouteHandler() { result = request.getRouteHandler() }
override string getKind() { result = "header" }
RequestNode getRequest() { result = request }
}
class RouteSetup extends DataFlow::CallNode, Http::Servers::StandardRouteSetup {
ServerDefinition server;
DataFlow::Node handler;
RouteSetup() {
server.ref() = this and
handler = this.getLastArgument()
or
server.ref().getAMethodCall() = this and
this.getCalleeName().regexpMatch("on(ce)?") and
this.getArgument(0).getStringValue() = "request" and
handler = this.getArgument(1)
}
override DataFlow::SourceNode getARouteHandler() {
result = this.getARouteHandler(DataFlow::TypeBackTracker::end())
}
private DataFlow::SourceNode getARouteHandler(DataFlow::TypeBackTracker t) {
t.start() and
result = handler.getALocalSource()
or
exists(DataFlow::TypeBackTracker t2, DataFlow::SourceNode succ |
succ = this.getARouteHandler(t2)
|
result = succ.backtrack(t2, t)
or
t = t2 and
Http::routeHandlerStep(result, succ)
)
}
override DataFlow::Node getServer() { result = server }
/**
* Gets the expression for the handler registered by this setup.
*/
DataFlow::Node getRouteHandlerNode() { result = handler }
}
overlay[global]
abstract private class HeaderDefinition extends Http::Servers::StandardHeaderDefinition {
ResponseNode r;
HeaderDefinition() { this.getReceiver() = r }
override Http::RouteHandler getRouteHandler() { result = r.getRouteHandler() }
}
/**
* A call to the `setHeader` method of an HTTP response.
*/
private class SetHeader extends HeaderDefinition {
SetHeader() { this.getMethodName() = "setHeader" }
}
/**
* A call to the `writeHead` method of an HTTP response.
*/
private class WriteHead extends HeaderDefinition {
WriteHead() {
this.getMethodName() = "writeHead" and
this.getNumArgument() >= 1
}
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
this.getNumArgument() > 1 and
exists(DataFlow::SourceNode headers, string header |
headers.flowsTo(this.getLastArgument()) and
headers.hasPropertyWrite(header, headerValue) and
headerName = header.toLowerCase()
)
}
}
/**
* A call to a path-module method that preserves taint.
*/
private class PathFlowStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::CallNode call, string methodName |
call = NodeJSLib::Path::moduleMember(methodName).getACall() and
succ = call
|
// getters
methodName = "basename" and pred = call.getArgument(0)
or
methodName = "dirname" and pred = call.getArgument(0)
or
methodName = "extname" and pred = call.getArgument(0)
or
// transformers
methodName = "join" and pred = call.getAnArgument()
or
methodName = "normalize" and pred = call.getArgument(0)
or
methodName = "relative" and pred = call.getArgument([0 .. 1])
or
methodName = "resolve" and pred = call.getAnArgument()
or
methodName = "toNamespacedPath" and pred = call.getArgument(0)
)
}
}
/**
* A call to a fs-module method that preserves taint.
*/
private class FsFlowStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::CallNode call, string methodName |
call = FS::moduleMember(methodName).getACall()
|
methodName = "realpathSync" and
pred = call.getArgument(0) and
succ = call
or
methodName = "realpath" and
pred = call.getArgument(0) and
succ = call.getCallback(1).getParameter(1)
)
}
}
/**
* A model of taint propagation through `new Buffer` and `Buffer.from`.
*/
private class BufferTaintStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::InvokeNode invoke |
invoke = DataFlow::globalVarRef("Buffer").getAnInstantiation()
or
invoke = DataFlow::globalVarRef("Buffer").getAMemberInvocation("from")
|
pred = invoke.getArgument(0) and
succ = invoke
)
}
}
/**
* An expression passed as the first argument to the `write` or `end` method
* of an HTTP response.
*/
private class ResponseSendArgument extends Http::ResponseSendArgument {
Http::RouteHandler rh;
ResponseSendArgument() {
exists(DataFlow::MethodCallNode mcn, string m | m = "write" or m = "end" |
mcn.calls(any(ResponseNode e | e.getRouteHandler() = rh), m) and
this = mcn.getArgument(0) and
// don't mistake callback functions as data
not this.analyze().getAValue() instanceof AbstractFunction
)
}
override Http::RouteHandler getRouteHandler() { result = rh }
}
/**
* An expression that creates a new Node.js server.
*/
class ServerDefinition extends Http::Servers::StandardServerDefinition {
ServerDefinition() { isCreateServer(this) }
}
/** An expression that is passed as `http.request({ auth: <expr> }, ...)`. */
class Credentials extends CredentialsNode {
Credentials() {
exists(string http | http = "http" or http = "https" |
this = DataFlow::moduleMember(http, "request").getACall().getOptionArgument(0, "auth")
)
}
override string getCredentialsKind() { result = "credentials" }
}
/**
* A call a process-terminating function, such as `process.exit`.
*/
class ProcessTermination extends SensitiveAction, DataFlow::ValueNode {
override CallExpr astNode;
ProcessTermination() {
this = API::moduleImport("exit").getAnInvocation()
or
this = processMember("exit").getACall()
}
}
/**
* Holds if the `i`th parameter of method `methodName` of the Node.js
* `fs` module or the `fs-extra` module might represent a file path.
*
* For `fs`, we determine this by looking for an externs declaration for
* `fs.methodName` where the `i`th parameter's name is `filename` or
* `path` or a variation thereof.
*
* For `fs-extra`, we use a manually maintained list.
*/
private predicate fsFileParam(string methodName, int i) {
exists(ExternalMemberDecl decl, Function f, JSDocParamTag p, string n |
decl.hasQualifiedName("fs", methodName) and
f = decl.getInit() and
p.getDocumentedParameter() = f.getParameter(i).getAVariable() and
n = p.getName().toLowerCase()
|
n = "filename" or n.regexpMatch("(old|new|src|dst|)path")
)
or
fsExtraExtensionFileParam(methodName, i)
}
/**
* Holds if `methodName` is a function defined in the `fs-extra` library
* as an extension to node.js' `fs` module and parameter `i` of of the
* method might represent a file path.
*/
private predicate fsExtraExtensionFileParam(string methodName, int i) {
methodName = ["copy", "copySync", "copyFile", "cp", "copyFileSync", "cpSync"] and i = [0, 1]
or
methodName = ["move", "moveSync"] and i = [0, 1]
or
methodName = ["createFile", "createFileSync"] and i = 0
or
methodName = ["createSymLink", "createSymlinkSync"] and i = [0, 1]
or
methodName = ["ensureDir", "ensureDirSync"] and i = 0
or
methodName = ["mkdirs", "mkdirp", "mkdirsSync", "mkdirpSync"] and i = 0
or
methodName = ["outputFile", "outputFileSync"] and i = 0
or
methodName = ["readJson", "readJSON", "readJsonSync", "readJSONSync"] and i = 0
or
methodName = ["remove", "removeSync", "rmSync", "rm", "rmdir", "rmdirSync"] and i = 0
or
methodName =
[
"outputJSON", "outputJson", "writeJSON", "writeJson", "writeJSONSync", "writeJsonSync",
"outputJSONSync", "outputJsonSync"
] and
i = 0
or
methodName = ["ensureFile", "ensureFileSync"] and i = 0
or
methodName = ["ensureLink", "createLink", "ensureLinkSync", "createLinkSync"] and i = [0, 1]
or
methodName = ["ensureSymlink", "ensureSymlinkSync"] and i = [0, 1]
or
methodName = ["emptyDir", "emptyDirSync", "emptydir", "emptydirSync"] and i = 0
or
methodName = ["pathExists", "pathExistsSync"] and i = 0
or
methodName = ["lutimes", "lutimesSync"] and i = 0
or
methodName =
["opendir", "opendirSync", "openAsBlob", "statfs", "statfsSync", "open", "openSync"] and
i = 0
}
/**
* Holds if the `i`th parameter of method `methodName` of the Node.js
* `fs` module might represent a data parameter or buffer or a callback
* that receives the data.
*
* We determine this by looking for an externs declaration for
* `fs.methodName` where the `i`th parameter's name (`paramName`) is `data` or
* `buffer` or a `callback`.
*/
private predicate fsDataParam(string methodName, int i, string paramName) {
exists(ExternalMemberDecl decl, Function f, JSDocParamTag p |
decl.hasQualifiedName("fs", methodName) and
f = decl.getInit() and
p.getDocumentedParameter() = f.getParameter(i).getAVariable() and
paramName = p.getName().toLowerCase()
|
paramName = ["data", "buffer", "callback"]
)
}
/**
* Provides predicates for working with the "fs" module and its variants as a single module.
*/
module FS {
/**
* Gets a member `member` from module `fs` or its drop-in replacements `graceful-fs`, `fs-extra`, `original-fs`.
*/
DataFlow::SourceNode moduleMember(string member) {
result = fsModule(DataFlow::TypeTracker::end()).getAPropertyRead(member)
}
private DataFlow::SourceNode fsModule(DataFlow::TypeTracker t) {
exists(string moduleName |
moduleName = ["mz/fs", "original-fs", "fs-extra", "graceful-fs", "fs"]
|
result = DataFlow::moduleImport(moduleName)
or
// extra support for flexible names
result.asExpr().(Require).getArgument(0).mayHaveStringValue(moduleName)
) and
t.start()
or
t.start() and
(
result = DataFlow::moduleMember("fs", "promises")
or
result = DataFlow::moduleImport("fs/promises")
)
or
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode pred | pred = fsModule(t2) |
result = pred.track(t2, t)
or
t.continue() = t2 and
exists(Promisify::PromisifyAllCall promisifyAllCall |
result = promisifyAllCall and
pred.flowsTo(promisifyAllCall.getArgument(0))
)
or
// const fs = require('fs');
// let fs_copy = methods.reduce((obj, method) => {
// obj[method] = fs[method];
// return obj;
// }, {});
t.continue() = t2 and
exists(
DataFlow::MethodCallNode call, DataFlow::ParameterNode obj, DataFlow::SourceNode method
|
call.getMethodName() = "reduce" and
result = call and
obj = call.getABoundCallbackParameter(0, 0) and
obj.flowsTo(any(DataFlow::FunctionNode f).getAReturn()) and
exists(DataFlow::PropWrite write, DataFlow::PropRead read |
write = obj.getAPropertyWrite() and
method.flowsToExpr(write.getPropertyNameExpr()) and
method.flowsToExpr(read.getPropertyNameExpr()) and
read.getBase().getALocalSource() = fsModule(t2) and
write.getRhs() = maybePromisified(read)
)
)
)
}
}
/**
* A call to a method from module `fs`, `graceful-fs` or `fs-extra`.
*/
private class NodeJSFileSystemAccess extends FileSystemAccess, DataFlow::CallNode {
string methodName;
NodeJSFileSystemAccess() { this = maybePromisified(FS::moduleMember(methodName)).getACall() }
/**
* Gets the name of the called method.
*/
string getMethodName() { result = methodName }
override DataFlow::Node getAPathArgument() {
exists(int i | fsFileParam(methodName, i) | result = this.getArgument(i))
}
}
/** A write to the file system. */
private class NodeJSFileSystemAccessWrite extends FileSystemWriteAccess, NodeJSFileSystemAccess {
NodeJSFileSystemAccessWrite() {
methodName =
[
"appendFile", "appendFileSync", "write", "writeFile", "writeFileSync", "writeSync",
"link", "linkSync"
]
}
override DataFlow::Node getADataNode() {
exists(int i, string paramName | fsDataParam(methodName, i, paramName) |
if paramName = "callback"
then
exists(DataFlow::ParameterNode p |
p = this.getCallback(i).getAParameter() and
p.getName().regexpMatch("(?i)data|buffer|string") and
result = p
)
else result = this.getArgument(i)
)
}
}
/** A vectored write to the file system using `writev` or `writevSync` methods. */
private class NodeJSFileSystemVectorWrite extends FileSystemWriteAccess, NodeJSFileSystemAccess {
NodeJSFileSystemVectorWrite() { methodName = ["writev", "writevSync"] }
override DataFlow::Node getADataNode() { result = this.getArgument(1) }
}
/** A file system read. */
private class NodeJSFileSystemAccessRead extends FileSystemReadAccess, NodeJSFileSystemAccess {
NodeJSFileSystemAccessRead() { methodName = ["read", "readSync", "readFile", "readFileSync"] }
override DataFlow::Node getADataNode() {
if methodName.matches("%Sync")
then result = this
else (
exists(int i, string paramName | fsDataParam(methodName, i, paramName) |
if paramName = "callback"
then
exists(DataFlow::ParameterNode p |
p = this.getCallback(i).getAParameter() and
p.getName().regexpMatch("(?i)data|buffer|string") and
result = p
)
else result = this.getArgument(i)
)
or
exists(AwaitExpr await |
this.getEnclosingExpr() = await.getOperand() and
result = DataFlow::valueNode(await)
)
)
}
}
/** A vectored read to the file system. */
private class NodeJSFileSystemAccessVectorRead extends FileSystemReadAccess,
NodeJSFileSystemAccess
{
NodeJSFileSystemAccessVectorRead() { methodName = ["readv", "readvSync"] }
override DataFlow::Node getADataNode() {
result = this.getArgument(1)
or
exists(DataFlow::ArrayCreationNode array |
array.flowsTo(this.getArgument(1)) and
result = array.getAnElement()
)
}
}
/**
* A write to the file system, using a stream.
*/
private class FileStreamWrite extends FileSystemWriteAccess, DataFlow::CallNode {
NodeJSFileSystemAccess stream;
FileStreamWrite() {
stream.getMethodName() = "createWriteStream" and
exists(string method |
method = "write" or
method = "end"
|
this = stream.getAMemberCall(method)
)
}
override DataFlow::Node getADataNode() { result = this.getArgument(0) }
override DataFlow::Node getAPathArgument() { result = stream.getAPathArgument() }
}
/**
* A read from the file system using a stream.
*/
private class FileStreamRead extends FileSystemReadAccess, DataFlow::CallNode {
NodeJSFileSystemAccess stream;
string method;
FileStreamRead() {
stream.getMethodName() = "createReadStream" and
this = stream.getAMemberCall(method) and
(method = "read" or method = "pipe" or method = EventEmitter::on())
}
override DataFlow::Node getADataNode() {
method = "read" and
result = this
or
method = "pipe" and
result = this.getArgument(0)
or
method = EventEmitter::on() and
this.getArgument(0).mayHaveStringValue("data") and
result = this.getCallback(1).getParameter(0)
}
override DataFlow::Node getAPathArgument() { result = stream.getAPathArgument() }
}
/**
* A data flow node that contains a file name or an array of file names from the local file system.
*/
private class NodeJSFileNameSource extends FileNameSource {
NodeJSFileNameSource() {
exists(string name |
name = "readdir" or
name = "realpath"
|
this = FS::moduleMember(name).getACall().getCallback([1 .. 2]).getParameter(1) or
this = FS::moduleMember(name + "Sync").getACall()
)
}
}
/**
* Gets a possibly promisified (using `util.promisify`) version of the input `callback`.
*/
private DataFlow::SourceNode maybePromisified(DataFlow::SourceNode callback) {
result = callback
or
exists(Promisify::PromisifyCall promisify |
result = promisify and promisify.getArgument(0).getALocalSource() = callback
)
}
/**
* A call to `util.deprecate`, considered to introduce data flow from its first argument
* to its result.
*/
private class UtilDeprecateStep extends PreCallGraphStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::CallNode deprecate |
deprecate = DataFlow::moduleMember("util", "deprecate").getACall() or
deprecate = DataFlow::moduleImport("util-deprecate").getACall()
|
pred = deprecate.getArgument(0) and
succ = deprecate
)
}
}
/**
* A direct step from an named export to a property-read reading the exported value.
*/
private class ExportsStep extends PreCallGraphStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(Import imp, string name |
succ = DataFlow::valueNode(imp).(DataFlow::SourceNode).getAPropertyRead(name) and
pred = imp.getImportedModule().getAnExportedValue(name)
)
}
}
/**
* A call to a method from module `child_process`.
*/
private class ChildProcessMethodCall extends SystemCommandExecution, API::CallNode {
string methodName;
ChildProcessMethodCall() {
this =
API::moduleImport(["mz/child_process", "child_process"])
.getMember(methodName)
.getMaybePromisifiedCall()
}
private DataFlow::Node getACommandArgument(boolean shell) {
// check whether this is an invocation of an exec/spawn/fork method
(
shell = true and
(
methodName = "exec" or
methodName = "execSync"
)
or
shell = false and
methodName = ["execFile", "execFileSync", "spawn", "spawnSync", "fork"]
) and
// all of the above methods take the command as their first argument
result = this.getParameter(0).asSink()
}
override DataFlow::Node getACommandArgument() { result = this.getACommandArgument(_) }
override predicate isShellInterpreted(DataFlow::Node arg) {
arg = this.getACommandArgument(true)
}
override DataFlow::Node getArgumentList() {
methodName = ["execFile", "execFileSync", "fork", "spawn", "spawnSync"] and
// all of the above methods take the argument list as their second argument
result = this.getParameter(1).asSink()
}
override predicate isSync() { methodName.matches("%Sync") }
override DataFlow::Node getOptionsArg() {
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode and // looks like argumentlist
not result = this.getParameter(0).asSink() and
// fork/spawn and all sync methos always has options as the last argument
if
methodName.matches("fork%") or
methodName.matches("spawn%") or
methodName.matches("%Sync")
then result = this.getLastArgument()
else
// the rest (exec/execFile) has the options argument as their second last.
result = this.getParameter(this.getNumArgument() - 2).asSink()
}
}
/**
* A function that looks like a Node.js route handler.
*
* For example, this could be the function `function(req, res){...}`.
*/
class RouteHandlerCandidate extends Http::RouteHandlerCandidate {
RouteHandlerCandidate() {
exists(string request, string response |
(request = "request" or request = "req") and
(response = "response" or response = "res") and
// heuristic: parameter names match the Node.js documentation
astNode.getNumParameter() = 2 and
astNode.getParameter(0).getName() = request and
astNode.getParameter(1).getName() = response
|
// heuristic: not a class method (Node.js invokes this with a function call)
not astNode = any(MethodDefinition def).getBody()
)
}
}
/**
* A function that flows to a route setup.
*/
private class TrackedRouteHandlerCandidateWithSetup extends RouteHandler,
Http::Servers::StandardRouteHandler, DataFlow::FunctionNode
{
TrackedRouteHandlerCandidateWithSetup() { this = any(RouteSetup s).getARouteHandler() }
}
/**
* An invocation of a member from module `vm`
*/
class VmModuleMemberInvocation extends DataFlow::InvokeNode {
string memberName;
VmModuleMemberInvocation() { this = DataFlow::moduleMember("vm", memberName).getAnInvocation() }
/**
* Gets the code to be executed as part of this invocation.
*/
DataFlow::Node getACodeArgument() {
memberName in [
"Script", "SourceTextModule", "compileFunction", "runInContext", "runInNewContext",
"runInThisContext"
] and
// all of the above methods/constructors take the command as their first argument
result = this.getArgument(0)
}
}
/**
* A call that looks like a route setup on a Node.js server.
*
* For example, this could be the call `server.on("request", handler)`
* where it is unknown if `server` is a Node.js server.
*/
class RouteSetupCandidate extends Http::RouteSetupCandidate, DataFlow::MethodCallNode {
DataFlow::ValueNode arg;
RouteSetupCandidate() {
this.getMethodName() = "createServer" and
arg = this.getLastArgument()
or
this.getMethodName().regexpMatch("on(ce)?") and
this.getArgument(0).mayHaveStringValue("request") and
arg = this.getArgument(1)
}
override DataFlow::ValueNode getARouteHandlerArg() { result = arg }
}
/**
* A data flow node that is an HTTP or HTTPS client request made by a Node.js application,
* for example `http.request(url)`.
*/
class NodeJSClientRequest extends ClientRequest instanceof NodeJSClientRequest::Range { }
module NodeJSClientRequest {
/**
* A data flow node that is an HTTP or HTTPS client request made by a Node.js application,
* for example `http.request(url)`.
*
* Extend this class to add support for new Node.js client request APIs.
*/
abstract class Range extends ClientRequest::Range { }
}
/**
* A model of a URL request in the Node.js `http` library.
*/
private class NodeHttpUrlRequest extends NodeJSClientRequest::Range, NodeJSEventEmitter {
DataFlow::Node url;
NodeHttpUrlRequest() {
exists(string moduleName, DataFlow::SourceNode callee | this = callee.getACall() |
(moduleName = "http" or moduleName = "https") and
(
callee = DataFlow::moduleMember(moduleName, any(Http::RequestMethodName m).toLowerCase())
or
callee = DataFlow::moduleMember(moduleName, "request")
) and
url = this.getArgument(0)
)
}
override DataFlow::Node getUrl() { result = url }
override DataFlow::Node getHost() {
exists(string name |
name = "host" or
name = "hostname"
|
result = this.getOptionArgument(1, name)
)
}
override DataFlow::Node getADataNode() {
exists(string name | name = "write" or name = "end" |
result = this.(DataFlow::SourceNode).getAMethodCall(name).getArgument(0)
)
}
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
promise = false and
exists(DataFlow::ParameterNode res, DataFlow::CallNode onData |
res = this.getCallback(1).getParameter(0) and
onData = res.getAMethodCall(EventEmitter::on()) and
onData.getArgument(0).mayHaveStringValue("data") and
result = onData.getCallback(1).getParameter(0) and
responseType = "arraybuffer"
)
}
}
/**
* A data flow node that is registered as a callback for an HTTP or HTTPS request made by a Node.js process, for example the function `handler` in `http.request(url).on(message, handler)`.
*/
class ClientRequestHandler extends DataFlow::FunctionNode {
string handledEvent;
NodeJSClientRequest clientRequest;
ClientRequestHandler() {
exists(DataFlow::MethodCallNode mcn |
clientRequest.getAMethodCall(EventEmitter::on()) = mcn and
mcn.getArgument(0).mayHaveStringValue(handledEvent) and
this.flowsTo(mcn.getArgument(1))
)
or
this.flowsTo(clientRequest.(DataFlow::CallNode).getLastArgument()) and
handledEvent = "connection"
}
/**
* Gets the name of an event this callback is registered for.
*/
string getAHandledEvent() { result = handledEvent }
/**
* Gets a request this callback is registered for.
*/
NodeJSClientRequest getClientRequest() { result = clientRequest }
}
/**
* A data flow node that is the parameter of a response callback for an HTTP or HTTPS request made by a Node.js process, for example `res` in `http.request(url).on('response', (res) => {})`.
*/
private class ClientRequestResponseEvent extends RemoteFlowSource, DataFlow::ParameterNode {
ClientRequestResponseEvent() {
exists(ClientRequestHandler handler |
this = handler.getParameter(0) and
handler.getAHandledEvent() = "response"
)
}
override string getSourceType() { result = "NodeJSClientRequest response event" }
}
/**
* A data flow node that is the parameter of a data callback for an HTTP or HTTPS request made by a Node.js process, for example `chunk` in `http.request(url).on('response', (res) => {res.on('data', (chunk) => {})})`.
*/
private class ClientRequestDataEvent extends RemoteFlowSource {
ClientRequestDataEvent() {
exists(DataFlow::MethodCallNode mcn, ClientRequestResponseEvent cr |
cr.getAMethodCall(EventEmitter::on()) = mcn and
mcn.getArgument(0).mayHaveStringValue("data") and
this = mcn.getCallback(1).getParameter(0)
)
}
override string getSourceType() { result = "NodeJSClientRequest data event" }
}
/**
* A data flow node that is a login callback for an HTTP or HTTPS request made by a Node.js process.
*/
private class ClientRequestLoginHandler extends ClientRequestHandler {
ClientRequestLoginHandler() { this.getAHandledEvent() = "login" }
}
/**
* A data flow node that is a parameter of a login callback for an HTTP or HTTPS request made by a Node.js process, for example `res` in `http.request(url).on('login', (res, callback) => {})`.
*/
private class ClientRequestLoginEvent extends RemoteFlowSource {
ClientRequestLoginEvent() {
exists(ClientRequestLoginHandler handler | this = handler.getParameter(0))
}
override string getSourceType() { result = "NodeJSClientRequest login event" }
}
/**
* A data flow node that is the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `callback` in `http.request(url).on('login', (res, callback) => {})`.
*/
class ClientRequestLoginCallback extends DataFlow::ParameterNode {
ClientRequestLoginCallback() {
exists(ClientRequestLoginHandler handler | this = handler.getParameter(1))
}
}
/**
* A data flow node that is the username passed to the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `username` in `http.request(url).on('login', (res, cb) => {cb(username, password)})`.
*/
private class ClientRequestLoginUsername extends CredentialsNode {
ClientRequestLoginUsername() {
exists(ClientRequestLoginCallback callback | this = callback.getACall().getArgument(0))
}
override string getCredentialsKind() { result = "user name" }
}
/**
* A data flow node that is the password passed to the login callback provided by an HTTP or HTTPS request made by a Node.js process, for example `password` in `http.request(url).on('login', (res, cb) => {cb(username, password)})`.
*/
private class ClientRequestLoginPassword extends CredentialsNode {
ClientRequestLoginPassword() {
exists(ClientRequestLoginCallback callback | this = callback.getACall().getArgument(1))
}
override string getCredentialsKind() { result = "password" }
}
/**
* A data flow node that is the parameter of an error callback for an HTTP or HTTPS request made by a Node.js process, for example `err` in `http.request(url).on('error', (err) => {})`.
*/
private class ClientRequestErrorEvent extends RemoteFlowSource {
ClientRequestErrorEvent() {
exists(ClientRequestHandler handler |
this = handler.getParameter(0) and
handler.getAHandledEvent() = "error"
)
}
override string getSourceType() { result = "NodeJSClientRequest error event" }
}
/**
* An NodeJS EventEmitter instance.
* Events dispatched on this EventEmitter will be handled by event handlers registered on this EventEmitter.
* (That is opposed to e.g. SocketIO, which implements the same interface, but where events cross object boundaries).
*/
abstract class NodeJSEventEmitter extends EventEmitter::Range {
/**
* Get a Node that refers to a NodeJS EventEmitter instance.
*/
DataFlow::SourceNode ref() { result = EventEmitter::trackEventEmitter(this) }
}
/**
* Gets an import of the NodeJS EventEmitter.
*/
private API::Node getAnEventEmitterImport() {
result = API::moduleImport("events") or
result = API::moduleImport("events").getMember("EventEmitter")
}
/**
* An instance of an EventEmitter that is imported through the 'events' module.
*/
private class ImportedNodeJSEventEmitter extends NodeJSEventEmitter {
ImportedNodeJSEventEmitter() { this = getAnEventEmitterImport().getAnInstantiation() }
}
/**
* The NodeJS `process` object as an EventEmitter subclass.
*/
private class ProcessAsNodeJSEventEmitter extends NodeJSEventEmitter {
ProcessAsNodeJSEventEmitter() { this = process() }
}
/**
* A class that extends EventEmitter.
*/
private class EventEmitterSubClass extends DataFlow::ClassNode {
EventEmitterSubClass() {
this.getASuperClassNode() = getAnEventEmitterImport().getAValueReachableFromSource() or
this.getADirectSuperClass() instanceof EventEmitterSubClass
}
}
/**
* An instantiation of a class that extends EventEmitter.
*
* By extending `NodeJSEventEmitter' we get data-flow on the events passing through this EventEmitter.
*/
class CustomEventEmitter extends NodeJSEventEmitter {
EventEmitterSubClass clazz;
CustomEventEmitter() {
if exists(clazz.getAClassReference().getAnInstantiation())
then this = clazz.getAClassReference().getAnInstantiation()
else
// In case there are no explicit instantiations of the clazz, then we still want to track data flow between `this` nodes.
// This cannot produce false flow as the `.ref()` method below is always used when creating event-registrations/event-dispatches.
this = clazz
}
override DataFlow::SourceNode ref() {
result = NodeJSEventEmitter.super.ref() and not this = clazz
or
result = clazz.getAReceiverNode()
}
}
/**
* An HTTP request event handler parameter as an EventEmitter, for
* example the function `emitter` in either of the following:
*
* ```
* http.request(x, emitter => {...})
* ```
*
* ```
* http.request(...).on(y, emitter => { ...})
* ```
*/
private class ClientRequestEventEmitter extends NodeJSEventEmitter {
ClientRequestEventEmitter() {
exists(ClientRequestHandler handler |
not handler.getAHandledEvent() = "error" and
this = handler.getAParameter()
)
}
}
/**
* A registration of an event handler on a NodeJS EventEmitter instance.
*/
private class NodeJSEventRegistration extends EventRegistration::DefaultEventRegistration,
DataFlow::MethodCallNode
{
override NodeJSEventEmitter emitter;
NodeJSEventRegistration() { this = emitter.ref().getAMethodCall(EventEmitter::on()) }
}
/**
* A dispatch of an event on a NodeJS EventEmitter instance.
*/
private class NodeJSEventDispatch extends EventDispatch::DefaultEventDispatch,
DataFlow::MethodCallNode
{
override NodeJSEventEmitter emitter;
NodeJSEventDispatch() { this = emitter.ref().getAMethodCall("emit") }
}
/**
* An instance of net.createServer(), which creates a new TCP/IPC server.
*/
class NodeJSNetServer extends DataFlow::InvokeNode {
NodeJSNetServer() {
this = DataFlow::moduleMember(["net", "tls"], "createServer").getAnInvocation()
}
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
t.start() and result = this
or
exists(DataFlow::TypeTracker t2 | result = this.ref(t2).track(t2, t))
}
/**
* Gets a reference to this server.
*/
DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }
}
/**
* A connection opened on a NodeJS net server.
*/
private class NodeJSNetServerConnection extends EventEmitter::Range {
NodeJSNetServerConnection() {
exists(NodeJSNetServer server |
exists(DataFlow::MethodCallNode call |
call = server.ref().getAMethodCall("on") and
call.getArgument(0).mayHaveStringValue("connection")
|
this = call.getCallback(1).getParameter(0)
)
or
this = server.getCallback([0, 1]).getParameter(0)
)
}
DataFlow::SourceNode ref() { result = EventEmitter::trackEventEmitter(this) }
}
/**
* A registration of an event handler on a NodeJS net server instance.
*/
private class NodeJSNetServerRegistration extends EventRegistration::DefaultEventRegistration,
DataFlow::MethodCallNode
{
override NodeJSNetServerConnection emitter;
NodeJSNetServerRegistration() { this = emitter.ref().getAMethodCall(EventEmitter::on()) }
}
/**
* A data flow node representing data received from a client to a NodeJS net server, viewed as remote user input.
*/
private class NodeJSNetServerItemAsRemoteFlow extends RemoteFlowSource {
NodeJSNetServerItemAsRemoteFlow() {
this = any(NodeJSNetServerRegistration reg).getReceivedItem(_)
}
override string getSourceType() { result = "NodeJS server" }
}
/**
* An instantiation of the `respjs` library, which is an EventEmitter.
*/
private class RespJS extends NodeJSEventEmitter {
RespJS() { this = API::moduleImport("respjs").getAnInstantiation() }
}
/**
* A event dispatch that serializes the input data and emits the result on the "data" channel.
*/
private class RespWrite extends EventDispatch::DefaultEventDispatch, DataFlow::MethodCallNode {
override RespJS emitter;
RespWrite() { this = emitter.ref().getAMethodCall("write") }
override string getChannel() { result = "data" }
override DataFlow::Node getSentItem(int i) { i = 0 and result = this.getArgument(i) }
}
/**
* Provides predicates for working with the "path" module and its platform-specific instances as a single module.
*/
module Path {
/**
* Gets a node that imports the "path" module, or one of its platform-specific instances.
*/
DataFlow::SourceNode moduleImport() {
result = DataFlow::moduleImport("path") or
result = DataFlow::moduleMember("path", "posix") or
result = DataFlow::moduleMember("path", "win32")
}
/**
* Gets an access to member `member` of the "path" module, or one of its platform-specific instances.
*/
DataFlow::SourceNode moduleMember(string member) {
result = moduleImport().getAPropertyRead(member)
}
}
/** A read of `process.env`, considered as a threat-model source. */
private class ProcessEnvThreatSource extends ThreatModelSource::Range {
ProcessEnvThreatSource() { this = NodeJSLib::process().getAPropertyRead("env") }
override string getThreatModel() { result = "environment" }
override string getSourceType() { result = "process.env" }
}
}