mirror of
https://github.com/github/codeql.git
synced 2025-12-17 09:13:20 +01:00
495 lines
17 KiB
Plaintext
495 lines
17 KiB
Plaintext
/** Provides classes for working with Node.js modules. */
|
|
|
|
import javascript
|
|
private import NodeModuleResolutionImpl
|
|
private import semmle.javascript.DynamicPropertyAccess as DynamicPropertyAccess
|
|
private import semmle.javascript.internal.CachedStages
|
|
private import semmle.javascript.dataflow.internal.DataFlowNode
|
|
|
|
/**
|
|
* A Node.js module.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```
|
|
* const fs = require('fs');
|
|
* for (var i=2;i<process.argv.length; ++i)
|
|
* process.stdout.write(fs.readFileSync(process.argv[i], 'utf8'));
|
|
* ```
|
|
*/
|
|
class NodeModule extends Module {
|
|
NodeModule() {
|
|
is_module(this) and
|
|
is_nodejs(this)
|
|
}
|
|
|
|
/** Gets the `module` variable of this module. */
|
|
Variable getModuleVariable() { result = this.getScope().getVariable("module") }
|
|
|
|
/** Gets the `exports` variable of this module. */
|
|
Variable getExportsVariable() { result = this.getScope().getVariable("exports") }
|
|
|
|
/** Gets the scope induced by this module. */
|
|
override ModuleScope getScope() { result.getScopeElement() = this }
|
|
|
|
/**
|
|
* Gets an abstract value representing one or more values that may flow
|
|
* into this module's `module.exports` property.
|
|
*/
|
|
pragma[noinline]
|
|
DefiniteAbstractValue getAModuleExportsValue() {
|
|
result = this.getAModuleExportsProperty().getAValue()
|
|
}
|
|
|
|
pragma[noinline]
|
|
private AbstractProperty getAModuleExportsProperty() {
|
|
result.getBase().(AbstractModuleObject).getModule() = this and
|
|
result.getPropertyName() = "exports"
|
|
}
|
|
|
|
/**
|
|
* Gets an expression that is an alias for `module.exports`.
|
|
* For performance this predicate only computes relevant expressions (in `getAModuleExportsCandidate`).
|
|
* So if using this predicate - consider expanding the list of relevant expressions.
|
|
*/
|
|
DataFlow::AnalyzedNode getAModuleExportsNode() {
|
|
result = getAModuleExportsCandidate() and
|
|
result.getAValue() = this.getAModuleExportsValue()
|
|
}
|
|
|
|
/** Gets a symbol exported by this module. */
|
|
override string getAnExportedSymbol() {
|
|
result = super.getAnExportedSymbol()
|
|
or
|
|
result = this.getAnImplicitlyExportedSymbol()
|
|
or
|
|
// getters and the like.
|
|
exists(DataFlow::PropWrite pwn |
|
|
pwn.getBase() = this.getAModuleExportsNode() and
|
|
result = pwn.getPropertyName()
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getAnExportedValue(string name) {
|
|
// a property write whose base is `exports` or `module.exports`
|
|
exists(DataFlow::PropWrite pwn | result = pwn.getRhs() |
|
|
pwn.getBase() = this.getAModuleExportsNode() and
|
|
name = pwn.getPropertyName()
|
|
)
|
|
or
|
|
// a re-export using spread-operator. E.g. `const foo = require("./foo"); module.exports = {bar: bar, ...foo};`
|
|
exists(ObjectExpr obj | obj = this.getAModuleExportsNode().asExpr() |
|
|
result =
|
|
obj.getAProperty()
|
|
.(SpreadProperty)
|
|
.getInit()
|
|
.(SpreadElement)
|
|
.getOperand()
|
|
.flow()
|
|
.getALocalSource()
|
|
.asExpr()
|
|
.(Import)
|
|
.getImportedModule()
|
|
.getAnExportedValue(name)
|
|
)
|
|
or
|
|
// var imp = require('./imp');
|
|
// for (var name in imp){
|
|
// module.exports[name] = imp[name];
|
|
// }
|
|
exists(DynamicPropertyAccess::EnumeratedPropName read, Import imp, DataFlow::PropWrite write |
|
|
read.getSourceObject().getALocalSource().asExpr() = imp and
|
|
getASourceProp(read) = write.getRhs() and
|
|
write.getBase() = this.getAModuleExportsNode() and
|
|
write.getPropertyNameExpr().flow().getImmediatePredecessor*() = read and
|
|
result = imp.getImportedModule().getAnExportedValue(name)
|
|
)
|
|
or
|
|
// an externs definition (where appropriate)
|
|
exists(PropAccess pacc | result = DataFlow::valueNode(pacc) |
|
|
pacc.getBase() = this.getAModuleExportsNode().asExpr() and
|
|
name = pacc.getPropertyName() and
|
|
this.isExterns() and
|
|
exists(pacc.getDocumentation())
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getABulkExportedNode() {
|
|
Stages::Imports::ref() and
|
|
exists(DataFlow::PropWrite write |
|
|
write.getBase().asExpr() = this.getModuleVariable().getAnAccess() and
|
|
write.getPropertyName() = "exports" and
|
|
result = write.getRhs()
|
|
)
|
|
}
|
|
|
|
/** Gets a symbol that the module object inherits from its prototypes. */
|
|
private string getAnImplicitlyExportedSymbol() {
|
|
exists(ExternalConstructor ec | ec = this.getPrototypeOfExportedExpr() |
|
|
result = ec.getAMember().getName()
|
|
or
|
|
ec instanceof FunctionExternal and result = "prototype"
|
|
or
|
|
ec instanceof ArrayExternal and
|
|
exists(NumberLiteral nl | result = nl.getValue() and exists(result.toInt()))
|
|
)
|
|
}
|
|
|
|
/** Gets an externs declaration of the prototype object of a value exported by this module. */
|
|
private ExternalConstructor getPrototypeOfExportedExpr() {
|
|
exists(AbstractValue exported | exported = this.getAModuleExportsValue() |
|
|
result instanceof ObjectExternal
|
|
or
|
|
exported instanceof AbstractFunction and result instanceof FunctionExternal
|
|
or
|
|
exported instanceof AbstractOtherObject and result instanceof ArrayExternal
|
|
)
|
|
}
|
|
|
|
override predicate searchRoot(PathExpr path, Folder searchRoot, int priority) {
|
|
path.getEnclosingModule() = this and
|
|
exists(string pathval | pathval = path.getValue() |
|
|
// paths starting with `./` or `../` are resolved relative to the importing
|
|
// module's folder
|
|
pathval.regexpMatch("\\.\\.?(/.*)?") and
|
|
(searchRoot = this.getFile().getParentContainer() and priority = 0)
|
|
or
|
|
// paths starting with `/` are resolved relative to the file system root
|
|
pathval.matches("/%") and
|
|
(searchRoot.getBaseName() = "" and priority = 0)
|
|
or
|
|
// paths that do not start with `./`, `../` or `/` are resolved relative
|
|
// to `node_modules` folders
|
|
not pathval.regexpMatch("\\.\\.?(/.*)?|/.*") and
|
|
findNodeModulesFolder(this.getFile().getParentContainer(), searchRoot, priority)
|
|
)
|
|
}
|
|
}
|
|
|
|
// An copy of `DynamicPropertyAccess::EnumeratedPropName::getASourceProp` that doesn't use the callgraph.
|
|
// This avoids making the module-imports recursive with the callgraph.
|
|
private DataFlow::SourceNode getASourceProp(DynamicPropertyAccess::EnumeratedPropName prop) {
|
|
exists(DataFlow::Node base, DataFlow::Node key |
|
|
exists(DynamicPropertyAccess::DynamicPropRead read |
|
|
not read.hasDominatingAssignment() and
|
|
base = read.getBase() and
|
|
key = read.getPropertyNameNode() and
|
|
result = read
|
|
) and
|
|
prop.getASourceObjectRef().flowsTo(base) and
|
|
key.getImmediatePredecessor*() = prop
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets an expression that syntactically could be a alias for `module.exports`.
|
|
* This predicate exists to reduce the size of `getAModuleExportsNode`,
|
|
* while keeping all the tuples that could be relevant in later computations.
|
|
*/
|
|
pragma[noinline]
|
|
private DataFlow::Node getAModuleExportsCandidate() {
|
|
// A bit of manual magic
|
|
result = any(DataFlow::PropWrite w).getBase()
|
|
or
|
|
result = DataFlow::valueNode(any(PropAccess p | exists(p.getPropertyName())).getBase())
|
|
or
|
|
result = DataFlow::valueNode(any(ObjectExpr obj))
|
|
}
|
|
|
|
/**
|
|
* Holds if `nodeModules` is a folder of the form `<prefix>/node_modules`, where
|
|
* `<prefix>` is a (not necessarily proper) prefix of `f` and does not end in `/node_modules`,
|
|
* and `distance` is the number of path elements of `f` that are missing from `<prefix>`.
|
|
*
|
|
* This predicate implements the `NODE_MODULES_PATHS` procedure from the
|
|
* [specification of `require.resolve`](https://nodejs.org/api/modules.html#modules_all_together).
|
|
*
|
|
* For example, if `f` is `/a/node_modules/b`, we get the following results:
|
|
*
|
|
* <table border="1">
|
|
* <tr><th><code>nodeModules</code></th><th><code>distance</code></th></tr>
|
|
* <tr><td><code>/a/node_modules/b/node_modules</code></td><td>0</td></tr>
|
|
* <tr><td><code>/a/node_modules</code></td><td>2</td></tr>
|
|
* <tr><td><code>/node_modules</code></td><td>3</td></tr>
|
|
* </table>
|
|
*/
|
|
predicate findNodeModulesFolder(Folder f, Folder nodeModules, int distance) {
|
|
nodeModules = f.getFolder("node_modules") and
|
|
not f.getBaseName() = "node_modules" and
|
|
distance = 0
|
|
or
|
|
findNodeModulesFolder(f.getParentContainer(), nodeModules, distance - 1)
|
|
}
|
|
|
|
/**
|
|
* A Node.js `require` variable.
|
|
*/
|
|
private class RequireVariable extends Variable {
|
|
RequireVariable() {
|
|
this = any(ModuleScope m).getVariable("require")
|
|
or
|
|
// cover cases where we failed to detect Node.js code
|
|
this.(GlobalVariable).getName() = "require"
|
|
or
|
|
// track through assignments to other variables
|
|
this.getAnAssignedExpr().(VarAccess).getVariable() instanceof RequireVariable
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Holds if module `m` is in file `f`.
|
|
*/
|
|
private predicate moduleInFile(Module m, File f) { m.getFile() = f }
|
|
|
|
private predicate isModuleModule(EarlyStageNode nd) {
|
|
exists(ImportDeclaration imp | imp.getImportedPath().getValue() = "module" |
|
|
nd = TDestructuredModuleImportNode(imp)
|
|
or
|
|
nd = TValueNode(imp.getASpecifier().(ImportNamespaceSpecifier))
|
|
)
|
|
or
|
|
exists(EarlyStageNode other |
|
|
isModuleModule(other) and
|
|
DataFlow::localFlowStep(other, nd)
|
|
)
|
|
}
|
|
|
|
private predicate isCreateRequire(EarlyStageNode nd) {
|
|
exists(PropAccess prop |
|
|
isModuleModule(TValueNode(prop.getBase())) and
|
|
prop.getPropertyName() = "createRequire" and
|
|
nd = TValueNode(prop)
|
|
)
|
|
or
|
|
exists(PropertyPattern prop |
|
|
isModuleModule(TValueNode(prop.getObjectPattern())) and
|
|
prop.getName() = "createRequire" and
|
|
nd = TValueNode(prop.getValuePattern())
|
|
)
|
|
or
|
|
exists(ImportDeclaration decl, NamedImportSpecifier spec |
|
|
decl.getImportedPath().getValue() = "module" and
|
|
spec = decl.getASpecifier() and
|
|
spec.getImportedName() = "createRequire" and
|
|
nd = TValueNode(spec)
|
|
)
|
|
or
|
|
exists(EarlyStageNode other |
|
|
isCreateRequire(other) and
|
|
DataFlow::localFlowStep(other, nd)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if `nd` may refer to `require`, either directly or modulo local data flow.
|
|
*/
|
|
cached
|
|
private predicate isRequire(EarlyStageNode nd) {
|
|
exists(VarAccess access |
|
|
access = any(RequireVariable v).getAnAccess() and
|
|
nd = TValueNode(access) and
|
|
// `mjs` files explicitly disallow `require`
|
|
not access.getFile().getExtension() = "mjs"
|
|
)
|
|
or
|
|
exists(EarlyStageNode other |
|
|
isRequire(other) and
|
|
DataFlow::localFlowStep(other, nd)
|
|
)
|
|
or
|
|
// `import { createRequire } from 'module';`.
|
|
// specialized to ES2015 modules to avoid recursion in the `DataFlow::moduleImport()` predicate and to avoid
|
|
// negative recursion between `Import.getImportedModuleNode()` and `Import.getImportedModule()`, and
|
|
// to avoid depending on `SourceNode` as this would make `SourceNode::Range` recursive.
|
|
exists(CallExpr call |
|
|
isCreateRequire(TValueNode(call.getCallee())) and
|
|
nd = TValueNode(call)
|
|
)
|
|
or
|
|
// `$.require('underscore');`.
|
|
// NPM as supported in [XSJS files](https://www.npmjs.com/package/@sap/async-xsjs#npm-packages-support).
|
|
exists(MethodCallExpr require |
|
|
require.getFile().getExtension() = ["xsjs", "xsjslib"] and
|
|
require.getCalleeName() = "require" and
|
|
require.getReceiver().(GlobalVarAccess).getName() = "$" and
|
|
nd = TValueNode(require.getCallee())
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A `require` import.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```
|
|
* require('fs')
|
|
* ```
|
|
*/
|
|
class Require extends CallExpr, Import {
|
|
Require() { isRequire(TValueNode(this.getCallee())) }
|
|
|
|
override PathExpr getImportedPath() { result = this.getArgument(0) }
|
|
|
|
override Module getEnclosingModule() { this = result.getAnImport() }
|
|
|
|
override Module resolveImportedPath() {
|
|
moduleInFile(result, this.load(min(int prio | moduleInFile(_, this.load(prio)))))
|
|
or
|
|
not moduleInFile(_, this.load(_)) and
|
|
result = Import.super.resolveImportedPath()
|
|
}
|
|
|
|
/**
|
|
* Gets the file that is imported by this `require`.
|
|
*
|
|
* The result can be a JavaScript file, a JSON file or a `.node` file.
|
|
* Externs files are not treated differently from other files by this predicate.
|
|
*/
|
|
File getImportedFile() { result = this.load(min(int prio | exists(this.load(prio)))) }
|
|
|
|
/**
|
|
* Gets the file that this `require` refers to (which may not be a JavaScript file),
|
|
* using the root folder of priority `priority`.
|
|
*
|
|
* This predicate implements the specification of
|
|
* [`require.resolve`](https://nodejs.org/api/modules.html#modules_all_together),
|
|
* modified to allow additional JavaScript file extensions, such as `ts` and `jsx`.
|
|
*
|
|
* Module resolution order is modeled using the `priority` parameter as follows.
|
|
*
|
|
* Each candidate folder in which the path may be resolved is assigned
|
|
* a priority (this is actually done by `Module.searchRoot`, but we explain it
|
|
* here for completeness):
|
|
*
|
|
* - if the path starts with `'./'`, `'../'`, or `/`, it has a single candidate
|
|
* folder (the enclosing folder of the module for the former two, the file
|
|
* system root for the latter) of priority 0
|
|
* - otherwise, candidate folders are folders of the form `<prefix>/node_modules`
|
|
* such that `<prefix>` is a (not necessarily proper) ancestor of the enclosing
|
|
* folder of the module which is not itself named `node_modules`; the priority
|
|
* of a candidate folder is the number of steps from the enclosing folder of
|
|
* the module to `<prefix>`.
|
|
*
|
|
* To resolve an import of a path `p`, we consider each candidate folder `c` with
|
|
* priority `r` and resolve the import to the following files if they exist
|
|
* (in order of priority):
|
|
*
|
|
* <ul>
|
|
* <li> the file `c/p`;
|
|
* <li> the file `c/p.{tsx,ts,jsx,es6,es,mjs,cjs}`;
|
|
* <li> the file `c/p.js`;
|
|
* <li> the file `c/p.json`;
|
|
* <li> the file `c/p.node`;
|
|
* <li> if `c/p` is a folder:
|
|
* <ul>
|
|
* <li> if `c/p/package.json` exists and specifies a `main` module `m`:
|
|
* <ul>
|
|
* <li> the file `c/p/m`;
|
|
* <li> the file `c/p/m.{tsx,ts,jsx,es6,es,mjs,cjs}`;
|
|
* <li> the file `c/p/m.js`;
|
|
* <li> the file `c/p/m.json`;
|
|
* <li> the file `c/p/m.node`;
|
|
* </ul>
|
|
* <li> the file `c/p/index.{tsx,ts,jsx,es6,es,mjs,cjs}`;
|
|
* <li> the file `c/p/index.js`;
|
|
* <li> the file `c/p/index.json`;
|
|
* <li> the file `c/p/index.node`.
|
|
* </ul>
|
|
* </ul>
|
|
*
|
|
* The first four steps are factored out into predicate `loadAsFile`,
|
|
* the remainder into `loadAsDirectory`; both make use of an auxiliary
|
|
* predicate `tryExtensions` that handles the repeated distinction between
|
|
* `.js`, `.json` and `.node`.
|
|
*/
|
|
private File load(int priority) {
|
|
exists(int r | this.getEnclosingModule().searchRoot(this.getImportedPath(), _, r) |
|
|
result = loadAsFile(this, r, priority - prioritiesPerCandidate() * r) or
|
|
result =
|
|
loadAsDirectory(this, r,
|
|
priority - (prioritiesPerCandidate() * r + numberOfExtensions() + 1))
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getImportedModuleNode() { result = DataFlow::valueNode(this) }
|
|
}
|
|
|
|
/** An argument to `require` or `require.resolve`, considered as a path expression. */
|
|
private class RequirePath extends PathExprCandidate {
|
|
RequirePath() {
|
|
this = any(Require req).getArgument(0)
|
|
or
|
|
exists(MethodCallExpr reqres |
|
|
isRequire(TValueNode(reqres.getReceiver())) and
|
|
reqres.getMethodName() = "resolve" and
|
|
this = reqres.getArgument(0)
|
|
)
|
|
}
|
|
}
|
|
|
|
/** A constant path element appearing in a call to `require` or `require.resolve`. */
|
|
private class ConstantRequirePathElement extends PathExpr, ConstantString {
|
|
ConstantRequirePathElement() { this = any(RequirePath rp).getAPart() }
|
|
|
|
override string getValue() { result = this.getStringValue() }
|
|
}
|
|
|
|
/** A `__dirname` path expression. */
|
|
private class DirNamePath extends PathExpr, VarAccess {
|
|
DirNamePath() {
|
|
this.getName() = "__dirname" and
|
|
this.getVariable().getScope() instanceof ModuleScope
|
|
}
|
|
|
|
override string getValue() { result = this.getFile().getParentContainer().getAbsolutePath() }
|
|
}
|
|
|
|
/** A `__filename` path expression. */
|
|
private class FileNamePath extends PathExpr, VarAccess {
|
|
FileNamePath() {
|
|
this.getName() = "__filename" and
|
|
this.getVariable().getScope() instanceof ModuleScope
|
|
}
|
|
|
|
override string getValue() { result = this.getFile().getAbsolutePath() }
|
|
}
|
|
|
|
/**
|
|
* A path expression of the form `path.join(p, "...")` where
|
|
* `p` is also a path expression.
|
|
*/
|
|
private class JoinedPath extends PathExpr, @call_expr {
|
|
JoinedPath() {
|
|
exists(MethodCallExpr call | call = this |
|
|
call.getReceiver().(VarAccess).getName() = "path" and
|
|
call.getMethodName() = "join" and
|
|
call.getNumArgument() = 2 and
|
|
call.getArgument(0) instanceof PathExpr and
|
|
call.getArgument(1) instanceof ConstantString
|
|
)
|
|
}
|
|
|
|
override string getValue() {
|
|
exists(CallExpr call, PathExpr left, ConstantString right |
|
|
call = this and
|
|
left = call.getArgument(0) and
|
|
right = call.getArgument(1)
|
|
|
|
|
result = left.getValue() + "/" + right.getStringValue()
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A reference to the special `module` variable.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```
|
|
* module
|
|
* ```
|
|
*/
|
|
class ModuleAccess extends VarAccess {
|
|
ModuleAccess() { exists(ModuleScope ms | this = ms.getVariable("module").getAnAccess()) }
|
|
}
|