/** 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 /** * A Node.js module. * * Example: * * ``` * const fs = require('fs'); * for (var i=2;i/node_modules`, where * `` 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 ``. * * 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: * * * * * * *
nodeModulesdistance
/a/node_modules/b/node_modules0
/a/node_modules2
/node_modules3
*/ 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(DataFlow::Node nd) { exists(ImportDeclaration imp | imp.getImportedPath().getValue() = "module" and nd = [ DataFlow::destructuredModuleImportNode(imp), DataFlow::valueNode(imp.getASpecifier().(ImportNamespaceSpecifier)) ] ) or isModuleModule(nd.getAPredecessor()) } private predicate isCreateRequire(DataFlow::Node nd) { exists(PropAccess prop | isModuleModule(prop.getBase().flow()) and prop.getPropertyName() = "createRequire" and nd = prop.flow() ) or exists(PropertyPattern prop | isModuleModule(prop.getObjectPattern().flow()) and prop.getName() = "createRequire" and nd = prop.getValuePattern().flow() ) or exists(ImportDeclaration decl, NamedImportSpecifier spec | decl.getImportedPath().getValue() = "module" and spec = decl.getASpecifier() and spec.getImportedName() = "createRequire" and nd = spec.flow() ) or isCreateRequire(nd.getAPredecessor()) } /** * Holds if `nd` may refer to `require`, either directly or modulo local data flow. */ cached private predicate isRequire(DataFlow::Node nd) { nd.asExpr() = any(RequireVariable req).getAnAccess() and // `mjs` files explicitly disallow `require` not nd.getFile().getExtension() = "mjs" or isRequire(nd.getAPredecessor()) 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(call.getCallee().flow()) and nd = call.flow() ) } /** * A `require` import. * * Example: * * ``` * require('fs') * ``` */ class Require extends CallExpr, Import { Require() { isRequire(this.getCallee().flow()) } 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 `/node_modules` * such that `` 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 ``. * * 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): * *
    *
  • the file `c/p`; *
  • the file `c/p.{tsx,ts,jsx,es6,es,mjs,cjs}`; *
  • the file `c/p.js`; *
  • the file `c/p.json`; *
  • the file `c/p.node`; *
  • if `c/p` is a folder: *
      *
    • if `c/p/package.json` exists and specifies a `main` module `m`: *
        *
      • the file `c/p/m`; *
      • the file `c/p/m.{tsx,ts,jsx,es6,es,mjs,cjs}`; *
      • the file `c/p/m.js`; *
      • the file `c/p/m.json`; *
      • the file `c/p/m.node`; *
      *
    • the file `c/p/index.{tsx,ts,jsx,es6,es,mjs,cjs}`; *
    • the file `c/p/index.js`; *
    • the file `c/p/index.json`; *
    • the file `c/p/index.node`. *
    *
* * 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(reqres.getReceiver().flow()) 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()) } }