diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index 877b47651ef7..4e223ccc3ccc 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -5,6 +5,7 @@ import Customizations import semmle.javascript.Aliases import semmle.javascript.AMD +import semmle.javascript.ApiGraphs import semmle.javascript.Arrays import semmle.javascript.AST import semmle.javascript.BasicBlocks diff --git a/javascript/ql/src/semmle/javascript/ApiGraphs.qll b/javascript/ql/src/semmle/javascript/ApiGraphs.qll new file mode 100644 index 000000000000..4c92212df576 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/ApiGraphs.qll @@ -0,0 +1,746 @@ +/** + * Provides an implementation of _API graphs_, which are an abstract representation of the API + * surface used and/or defined by a code base. + * + * The nodes of the API graph represent definitions and uses of API components. The edges are + * directed and labeled; they specify how the components represented by nodes relate to each other. + * For example, if one of the nodes represents a definition of an API function, then there + * will be nodes corresponding to the function's parameters, which are connected to the function + * node by edges labeled `parameter `. + */ + +import javascript + +/** + * Provides classes and predicates for working with APIs defined or used in a database. + */ +module API { + /** + * An abstract representation of a definition or use of an API component such as a function + * exported by an npm package, a parameter of such a function, or its result. + */ + class Node extends Impl::TApiNode { + /** + * Gets a data-flow node corresponding to a use of the API component represented by this node. + * + * For example, `require('fs').readFileSync` is a use of the function `readFileSync` from the + * `fs` module, and `require('fs').readFileSync(file)` is a use of the result of that function. + * + * As another example, in the assignment `exports.plusOne = (x) => x+1` the two references to + * `x` are uses of the first parameter of `plusOne`. + */ + DataFlow::Node getAUse() { + exists(DataFlow::SourceNode src | Impl::use(this, src) | + Impl::trackUseNode(src).flowsTo(result) + ) + } + + /** + * Gets a data-flow node corresponding to the right-hand side of a definition of the API + * component represented by this node. + * + * For example, in the assignment `exports.plusOne = (x) => x+1`, the function expression + * `(x) => x+1` is the right-hand side of the definition of the member `plusOne` of + * the enclosing module, and the expression `x+1` is the right-had side of the definition of + * its result. + * + * Note that for parameters, it is the arguments flowing into that parameter that count as + * right-hand sides of the definition, not the declaration of the parameter itself. + * Consequently, in `require('fs').readFileSync(file)`, `file` is the right-hand + * side of a definition of the first parameter of `readFileSync` from the `fs` module. + */ + DataFlow::Node getARhs() { Impl::rhs(this, result) } + + /** + * Gets a node representing member `m` of this API component. + * + * For example, modules have an `exports` member representing their exports, and objects have + * their properties as members. + */ + bindingset[m] + bindingset[result] + Node getMember(string m) { result = getASuccessor(Label::member(m)) } + + /** + * Gets a node representing a member of this API component where the name of the member is + * not known statically. + */ + Node getUnknownMember() { result = getASuccessor(Label::unknownMember()) } + + /** + * Gets a node representing a member of this API component where the name of the member may + * or may not be known statically. + */ + Node getAMember() { + result = getASuccessor(Label::member(_)) or + result = getUnknownMember() + } + + /** + * Gets a node representing an instance of this API component, that is, an object whose + * constructor is the function represented by this node. + * + * For example, if this node represents a use of some class `A`, then there might be a node + * representing instances of `A`, typically corresponding to expressions `new A()` at the + * source level. + */ + Node getInstance() { result = getASuccessor(Label::instance()) } + + /** + * Gets a node representing the `i`th parameter of the function represented by this node. + */ + bindingset[i] + Node getParameter(int i) { result = getASuccessor(Label::parameter(i)) } + + /** + * Gets the number of parameters of the function represented by this node. + */ + int getNumParameter() { + result = + max(string s | exists(getASuccessor(Label::parameterByStringIndex(s))) | s.toInt()) + 1 + } + + /** + * Gets a node representing the last parameter of the function represented by this node. + */ + Node getLastParameter() { result = getParameter(getNumParameter() - 1) } + + /** + * Gets a node representing the receiver of the function represented by this node. + */ + Node getReceiver() { result = getASuccessor(Label::receiver()) } + + /** + * Gets a node representing a parameter or the receiver of the function represented by this + * node. + */ + Node getAParameter() { + result = getASuccessor(Label::parameterByStringIndex(_)) or + result = getReceiver() + } + + /** + * Gets a node representing the result of the function represented by this node. + */ + Node getReturn() { result = getASuccessor(Label::return()) } + + /** + * Gets a node representing the promised value wrapped in the `Promise` object represented by + * this node. + */ + Node getPromised() { result = getASuccessor(Label::promised()) } + + /** + * Gets a string representation of the lexicographically least among all shortest access paths + * from the root to this node. + */ + string getPath() { result = min(string p | p = getAPath(Impl::distanceFromRoot(this)) | p) } + + /** + * Gets a node such that there is an edge in the API graph between this node and the other + * one, and that edge is labeled with `lbl`. + */ + Node getASuccessor(string lbl) { Impl::edge(this, lbl, result) } + + /** + * Gets a node such that there is an edge in the API graph between that other node and + * this one, and that edge is labeled with `lbl` + */ + Node getAPredecessor(string lbl) { this = result.getASuccessor(lbl) } + + /** + * Gets a node such that there is an edge in the API graph between this node and the other + * one. + */ + Node getAPredecessor() { result = getAPredecessor(_) } + + /** + * Gets a node such that there is an edge in the API graph between that other node and + * this one. + */ + Node getASuccessor() { result = getASuccessor(_) } + + /** + * Holds if this node may take its value from `that` node. + * + * In other words, the value of a use of `that` may flow into the right-hand side of a + * definition of this node. + */ + predicate refersTo(Node that) { this.getARhs() = that.getAUse() } + + /** + * Gets the data-flow node that gives rise to this node, if any. + */ + DataFlow::Node getInducingNode() { + this = Impl::MkClassInstance(result) or + this = Impl::MkUse(result) or + this = Impl::MkDef(result) or + this = Impl::MkAsyncFuncResult(result) + } + + /** + * Holds if this node is located in file `path` between line `startline`, column `startcol`, + * and line `endline`, column `endcol`. + * + * For nodes that do not have a meaningful location, `path` is the empty string and all other + * parameters are zero. + */ + predicate hasLocationInfo(string path, int startline, int startcol, int endline, int endcol) { + getInducingNode().hasLocationInfo(path, startline, startcol, endline, endcol) + or + not exists(getInducingNode()) and + path = "" and + startline = 0 and + startcol = 0 and + endline = 0 and + endcol = 0 + } + + /** + * Gets a textual representation of this node. + */ + string toString() { + none() // defined in subclasses + } + + /** + * Gets a path of the given `length` from the root to this node. + */ + private string getAPath(int length) { + this instanceof Impl::MkRoot and + length = 0 and + result = "" + or + exists(Node pred, string lbl, string predpath | + Impl::edge(pred, lbl, this) and + lbl != "" and + predpath = pred.getAPath(length - 1) and + exists(string space | if length = 1 then space = "" else space = " " | + result = "(" + lbl + space + predpath + ")" and + // avoid producing strings longer than 1MB + result.length() < 1000 * 1000 + ) + ) and + length in [1 .. Impl::distanceFromRoot(this)] + } + } + + /** The root node of an API graph. */ + class Root extends Node, Impl::MkRoot { + override string toString() { result = "root" } + } + + /** A node corresponding to a definition of an API component. */ + class Definition extends Node, Impl::TDef { + override string toString() { result = "def " + getPath() } + } + + /** A node corresponding to the use of an API component. */ + class Use extends Node, Impl::TUse { + override string toString() { result = "use " + getPath() } + } + + /** Gets the root node. */ + Root root() { any() } + + /** Gets a node corresponding to an import of module `m`. */ + Node moduleImport(string m) { + result = Impl::MkModuleImport(m) or + result = Impl::MkModuleImport(m).(Node).getMember("default") + } + + /** Gets a node corresponding to an export of module `m`. */ + Node moduleExport(string m) { result = Impl::MkModuleDef(m).(Node).getMember("exports") } + + /** + * An API entry point. + * + * Extend this class to define additional API entry points other than modules. + * Typical examples include global variables. + */ + abstract class EntryPoint extends string { + bindingset[this] + EntryPoint() { any() } + + /** Gets a data-flow node that uses this entry point. */ + abstract DataFlow::SourceNode getAUse(); + + /** Gets a data-flow node that defines this entry point. */ + abstract DataFlow::Node getARhs(); + } + + /** + * Provides the actual implementation of API graphs, cached for performance. + * + * Ideally, we'd like nodes to correspond to (global) access paths, with edge labels + * corresponding to extending the access path by one element. We also want to be able to map + * nodes to their definitions and uses in the data-flow graph, and this should happen modulo + * (inter-procedural) data flow. + * + * This, however, is not easy to implement, since access paths can have unbounded length + * and we need some way of recognizing cycles to avoid non-termination. Unfortunately, expressing + * a condition like "this node hasn't been involved in constructing any predecessor of + * this node in the API graph" without negative recursion is tricky. + * + * So instead most nodes are directly associated with a data-flow node, representing + * either a use or a definition of an API component. This ensures that we only have a finite + * number of nodes. However, we can now have multiple nodes with the same access + * path, which are essentially indistinguishable for a client of the API. + * + * On the other hand, a single node can have multiple access paths (which is, of + * course, unavoidable). We pick as canonical the alphabetically least access path with + * shortest length. + */ + cached + private module Impl { + cached + newtype TApiNode = + MkRoot() or + MkModuleDef(string m) { exists(MkModuleExport(m)) } or + MkModuleUse(string m) { exists(MkModuleImport(m)) } or + MkModuleExport(string m) { + exists(Module mod | mod = importableModule(m) | + // exclude modules that don't actually export anything + exports(m, _) + or + exports(m, _, _) + or + exists(NodeModule nm | nm = mod | + exists(SSA::implicitInit([nm.getModuleVariable(), nm.getExportsVariable()])) + ) + ) + } or + MkModuleImport(string m) { imports(_, m) } or + MkClassInstance(DataFlow::ClassNode cls) { cls = trackDefNode(_) and hasSemantics(cls) } or + MkAsyncFuncResult(DataFlow::FunctionNode f) { + f = trackDefNode(_) and f.getFunction().isAsync() and hasSemantics(f) + } or + MkDef(DataFlow::Node nd) { rhs(_, _, nd) } or + MkUse(DataFlow::Node nd) { use(_, _, nd) } or + MkCanonicalNameDef(CanonicalName n) { isDefined(n) } or + MkCanonicalNameUse(CanonicalName n) { isUsed(n) } + + class TDef = MkModuleDef or TNonModuleDef; + + class TNonModuleDef = + MkModuleExport or MkClassInstance or MkAsyncFuncResult or MkDef or MkCanonicalNameDef; + + class TUse = MkModuleUse or MkModuleImport or MkUse or MkCanonicalNameUse; + + private predicate hasSemantics(DataFlow::Node nd) { not nd.getTopLevel().isExterns() } + + /** Holds if `imp` is an import of module `m`. */ + private predicate imports(DataFlow::Node imp, string m) { + imp = DataFlow::moduleImport(m) and + // path must not start with a dot or a slash + m.regexpMatch("[^./].*") and + hasSemantics(imp) + } + + /** Gets the definition of module `m`. */ + private Module importableModule(string m) { + exists(NPMPackage pkg, PackageJSON json | + json = pkg.getPackageJSON() and not json.isPrivate() + | + result = pkg.getMainModule() and + not result.isExterns() and + m = pkg.getPackageName() + ) + } + + private predicate isUsed(CanonicalName n) { + exists(n.(TypeName).getAnAccess()) or + exists(n.(Namespace).getAnAccess()) + } + + private predicate isDefined(CanonicalName n) { + exists(ASTNode def | + def = n.(TypeName).getADefinition() or + def = n.(Namespace).getADefinition() + | + not def.isAmbient() + ) + } + + /** + * Holds if `rhs` is the right-hand side of a definition of a node that should have an + * incoming edge from `base` labeled `lbl` in the API graph. + */ + cached + predicate rhs(TApiNode base, string lbl, DataFlow::Node rhs) { + hasSemantics(rhs) and + ( + base = MkRoot() and + rhs = lbl.(EntryPoint).getARhs() + or + exists(string m, string prop | + base = MkModuleExport(m) and + lbl = Label::member(prop) and + exports(m, prop, rhs) + ) + or + exists(DataFlow::Node def, DataFlow::SourceNode pred | + rhs(base, def) and pred = trackDefNode(def) + | + exists(DataFlow::PropWrite pw | pw = pred.getAPropertyWrite() | + lbl = Label::memberFromRef(pw) and + rhs = pw.getRhs() + ) + or + exists(DataFlow::FunctionNode fn | fn = pred | + not fn.getFunction().isAsync() and + lbl = Label::return() and + rhs = fn.getAReturn() + ) + or + lbl = Label::promised() and + PromiseFlow::storeStep(rhs, pred, Promises::valueProp()) + ) + or + exists(DataFlow::ClassNode cls, string name | + base = MkClassInstance(cls) and + lbl = Label::member(name) and + rhs = cls.getInstanceMethod(name) + ) + or + exists(DataFlow::FunctionNode f | + base = MkAsyncFuncResult(f) and + lbl = Label::promised() and + rhs = f.getAReturn() + ) + or + exists(DataFlow::SourceNode src, DataFlow::InvokeNode invk | + use(base, src) and invk = trackUseNode(src).getAnInvocation() + | + exists(int i | + lbl = Label::parameter(i) and + rhs = invk.getArgument(i) + ) + or + lbl = Label::receiver() and + rhs = invk.(DataFlow::CallNode).getReceiver() + ) + or + exists(DataFlow::SourceNode src, DataFlow::PropWrite pw | + use(base, src) and pw = trackUseNode(src).getAPropertyWrite() and rhs = pw.getRhs() + | + lbl = Label::memberFromRef(pw) + ) + ) + } + + /** + * Holds if `rhs` is the right-hand side of a definition of node `nd`. + */ + cached + predicate rhs(TApiNode nd, DataFlow::Node rhs) { + exists(string m | nd = MkModuleExport(m) | exports(m, rhs)) + or + nd = MkDef(rhs) + or + exists(CanonicalName n | nd = MkCanonicalNameDef(n) | + rhs = n.(Namespace).getADefinition().flow() or + rhs = n.(CanonicalFunctionName).getADefinition().flow() + ) + } + + /** + * Holds if `ref` is a use of a node that should have an incoming edge from `base` labeled + * `lbl` in the API graph. + */ + cached + predicate use(TApiNode base, string lbl, DataFlow::Node ref) { + hasSemantics(ref) and + ( + base = MkRoot() and + ref = lbl.(EntryPoint).getAUse() + or + exists(DataFlow::SourceNode src, DataFlow::SourceNode pred | + use(base, src) and pred = trackUseNode(src) + | + // `module.exports` is special: it is a use of a def-node, not a use-node, + // so we want to exclude it here + (base instanceof TNonModuleDef or base instanceof TUse) and + lbl = Label::memberFromRef(ref) and + ref = pred.getAPropertyRead() + or + lbl = Label::instance() and + ref = pred.getAnInstantiation() + or + lbl = Label::return() and + ref = pred.getAnInvocation() + or + lbl = Label::promised() and + PromiseFlow::loadStep(pred, ref, Promises::valueProp()) + ) + or + exists(DataFlow::Node def, DataFlow::FunctionNode fn | + rhs(base, def) and fn = trackDefNode(def) + | + exists(int i | + lbl = Label::parameter(i) and + ref = fn.getParameter(i) + ) + or + lbl = Label::receiver() and + ref = fn.getReceiver() + ) + or + exists(DataFlow::Node def, DataFlow::ClassNode cls, int i | + rhs(base, def) and cls = trackDefNode(def) + | + lbl = Label::parameter(i) and + ref = cls.getConstructor().getParameter(i) + ) + or + exists(TypeName tn | + base = MkCanonicalNameUse(tn) and + lbl = Label::instance() and + ref = getANodeWithType(tn) + ) + ) + } + + /** + * Holds if `ref` is a use of node `nd`. + */ + cached + predicate use(TApiNode nd, DataFlow::Node ref) { + exists(string m, Module mod | nd = MkModuleDef(m) and mod = importableModule(m) | + ref = DataFlow::ssaDefinitionNode(SSA::implicitInit(mod.(NodeModule).getModuleVariable())) + or + ref = DataFlow::parameterNode(mod.(AmdModule).getDefine().getModuleParameter()) + ) + or + exists(string m, Module mod | nd = MkModuleExport(m) and mod = importableModule(m) | + ref = DataFlow::ssaDefinitionNode(SSA::implicitInit(mod.(NodeModule).getExportsVariable())) + or + ref = DataFlow::parameterNode(mod.(AmdModule).getDefine().getExportsParameter()) + or + exists(DataFlow::Node base | use(MkModuleDef(m), base) | + ref = trackUseNode(base).getAPropertyRead("exports") + ) + ) + or + exists(string m | + nd = MkModuleImport(m) and + ref = DataFlow::moduleImport(m) + ) + or + exists(DataFlow::ClassNode cls | nd = MkClassInstance(cls) | ref = cls.getAReceiverNode()) + or + nd = MkUse(ref) + or + exists(CanonicalName n | nd = MkCanonicalNameUse(n) | ref.asExpr() = n.getAnAccess()) + } + + /** Holds if module `m` exports `rhs`. */ + private predicate exports(string m, DataFlow::Node rhs) { + exists(Module mod | mod = importableModule(m) | + rhs = mod.(AmdModule).getDefine().getModuleExpr().flow() + or + exports(m, "default", rhs) + or + exists(ExportAssignDeclaration assgn | assgn.getTopLevel() = mod | + rhs = assgn.getExpression().flow() + ) + or + rhs = mod.(Closure::ClosureModule).getExportsVariable().getAnAssignedExpr().flow() + ) + } + + /** Holds if module `m` exports `rhs` under the name `prop`. */ + private predicate exports(string m, string prop, DataFlow::Node rhs) { + exists(ExportDeclaration exp | exp.getEnclosingModule() = importableModule(m) | + rhs = exp.getSourceNode(prop) + or + exists(Variable v | + exp.exportsAs(v, prop) and + rhs = v.getAnAssignedExpr().flow() + ) + ) + } + + private DataFlow::SourceNode trackUseNode(DataFlow::SourceNode nd, DataFlow::TypeTracker t) { + t.start() and + use(_, nd) and + result = nd + or + exists(DataFlow::TypeTracker t2 | result = trackUseNode(nd, t2).track(t2, t)) + } + + /** + * Gets a node that is inter-procedurally reachable from `nd`, which is a use of some node. + */ + cached + DataFlow::SourceNode trackUseNode(DataFlow::SourceNode nd) { + result = trackUseNode(nd, DataFlow::TypeTracker::end()) + } + + private DataFlow::SourceNode trackDefNode(DataFlow::Node nd, DataFlow::TypeBackTracker t) { + t.start() and + rhs(_, nd) and + result = nd.getALocalSource() + or + exists(DataFlow::TypeBackTracker t2 | result = trackDefNode(nd, t2).backtrack(t2, t)) + } + + /** + * Gets a node that inter-procedurally flows into `nd`, which is a definition of some node. + */ + cached + DataFlow::SourceNode trackDefNode(DataFlow::Node nd) { + result = trackDefNode(nd, DataFlow::TypeBackTracker::end()) + } + + private DataFlow::SourceNode getANodeWithType(TypeName tn) { + exists(string moduleName, string typeName | + tn.hasQualifiedName(moduleName, typeName) and + result.hasUnderlyingType(moduleName, typeName) + ) + } + + /** + * Holds if there is an edge from `pred` to `succ` in the API graph that is labeled with `lbl`. + */ + cached + predicate edge(TApiNode pred, string lbl, TApiNode succ) { + exists(string m | + pred = MkRoot() and + lbl = Label::mod(m) + | + succ = MkModuleDef(m) + or + succ = MkModuleUse(m) + ) + or + exists(string m | + pred = MkModuleDef(m) and + lbl = Label::member("exports") and + succ = MkModuleExport(m) + or + pred = MkModuleUse(m) and + lbl = Label::member("exports") and + succ = MkModuleImport(m) + ) + or + exists(DataFlow::SourceNode ref | + use(pred, lbl, ref) and + succ = MkUse(ref) + ) + or + exists(DataFlow::Node rhs | + rhs(pred, lbl, rhs) and + succ = MkDef(rhs) + ) + or + exists(DataFlow::Node def | + rhs(pred, def) and + lbl = Label::instance() and + succ = MkClassInstance(trackDefNode(def)) + ) + or + exists(CanonicalName cn | + pred = MkRoot() and + lbl = Label::mod(cn.getExternalModuleName()) + | + succ = MkCanonicalNameUse(cn) or + succ = MkCanonicalNameDef(cn) + ) + or + exists(CanonicalName cn1, CanonicalName cn2 | + cn2 = cn1.getAChild() and + lbl = Label::member(cn2.getName()) + | + (pred = MkCanonicalNameDef(cn1) or pred = MkCanonicalNameUse(cn1)) and + (succ = MkCanonicalNameDef(cn2) or succ = MkCanonicalNameUse(cn2)) + ) + or + exists(DataFlow::Node nd, DataFlow::FunctionNode f | + pred = MkDef(nd) and + f = trackDefNode(nd) and + lbl = Label::return() and + succ = MkAsyncFuncResult(f) + ) + } + + /** + * Holds if there is an edge from `pred` to `succ` in the API graph. + */ + private predicate edge(TApiNode pred, TApiNode succ) { edge(pred, _, succ) } + + /** Gets the shortest distance from the root to `nd` in the API graph. */ + cached + int distanceFromRoot(TApiNode nd) = shortestDistances(MkRoot/0, edge/2)(_, nd, result) + } + + import Label as EdgeLabel +} + +private module Label { + /** Gets the edge label for the module `m`. */ + bindingset[m] + bindingset[result] + string mod(string m) { result = "module " + m } + + /** Gets the `member` edge label for member `m`. */ + bindingset[m] + bindingset[result] + string member(string m) { result = "member " + m } + + /** Gets the `member` edge label for the unknown member. */ + string unknownMember() { result = "member *" } + + /** Gets the `member` edge label for the given property reference. */ + string memberFromRef(DataFlow::PropRef pr) { + exists(string pn | pn = pr.getPropertyName() | + result = member(pn) and + // only consider properties with alphanumeric(-ish) names, excluding special properties + // and properties whose names look like they are meant to be internal + pn.regexpMatch("(?!prototype$|__)[a-zA-Z_$][\\w\\-.$]*") + ) + or + not exists(pr.getPropertyName()) and + result = unknownMember() + } + + /** Gets the `instance` edge label. */ + string instance() { result = "instance" } + + /** + * Gets the `parameter` edge label for the parameter `s`. + * + * This is an internal helper predicate; use `parameter` instead. + */ + bindingset[result] + bindingset[s] + string parameterByStringIndex(string s) { + result = "parameter " + s and + s.toInt() >= 0 + } + + /** Gets the `parameter` edge label for the `i`th parameter. */ + bindingset[i] + string parameter(int i) { result = parameterByStringIndex(i.toString()) } + + /** Gets the `parameter` edge label for the receiver. */ + string receiver() { result = "parameter -1" } + + /** Gets the `return` edge label. */ + string return() { result = "return" } + + /** Gets the `promised` edge label connecting a promise to its contained value. */ + string promised() { result = "promised" } +} + +/** + * A CommonJS `module` or `exports` variable, considered as a source node. + */ +private class AdditionalSourceNode extends DataFlow::SourceNode::Range { + AdditionalSourceNode() { + exists(NodeModule m, Variable v | + v in [m.getModuleVariable(), m.getExportsVariable()] and + this = DataFlow::ssaDefinitionNode(SSA::implicitInit(v)) + ) + } +} diff --git a/javascript/ql/src/semmle/javascript/SSA.qll b/javascript/ql/src/semmle/javascript/SSA.qll index a3d74e4f7246..ac574e4ab10b 100644 --- a/javascript/ql/src/semmle/javascript/SSA.qll +++ b/javascript/ql/src/semmle/javascript/SSA.qll @@ -737,6 +737,9 @@ class SsaRefinementNode extends SsaPseudoDefinition, TRefinement { } module SSA { + /** Gets the SSA definition corresponding to the implicit initialization of `v`. */ + SsaImplicitInit implicitInit(SsaSourceVariable v) { result.getSourceVariable() = v } + /** Gets the SSA definition corresponding to `d`. */ SsaExplicitDefinition definition(VarDef d) { result.getDef() = d } diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index fb12c5ab55df..19eaf168c085 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -28,42 +28,29 @@ module SQL { * Provides classes modelling the (API compatible) `mysql` and `mysql2` packages. */ private module MySql { - private DataFlow::SourceNode mysql() { result = DataFlow::moduleImport(["mysql", "mysql2"]) } + /** Gets the package name `mysql` or `mysql2`. */ + API::Node mysql() { result = API::moduleImport(["mysql", "mysql2"]) } - private DataFlow::CallNode createPool() { result = mysql().getAMemberCall("createPool") } - - /** Gets a reference to a MySQL pool. */ - private DataFlow::SourceNode pool(DataFlow::TypeTracker t) { - t.start() and - result = createPool() - or - exists(DataFlow::TypeTracker t2 | result = pool(t2).track(t2, t)) - } + /** Gets a call to `mysql.createConnection`. */ + API::Node createConnection() { result = mysql().getMember("createConnection").getReturn() } - /** Gets a reference to a MySQL pool. */ - private DataFlow::SourceNode pool() { result = pool(DataFlow::TypeTracker::end()) } + /** Gets a call to `mysql.createPool`. */ + API::Node createPool() { result = mysql().getMember("createPool").getReturn() } - /** Gets a call to `mysql.createConnection`. */ - DataFlow::CallNode createConnection() { result = mysql().getAMemberCall("createConnection") } - - /** Gets a reference to a MySQL connection instance. */ - private DataFlow::SourceNode connection(DataFlow::TypeTracker t) { - t.start() and - ( - result = createConnection() - or - result = pool().getAMethodCall("getConnection").getABoundCallbackParameter(0, 1) - ) + /** Gets a data flow node that contains a freshly created MySQL connection instance. */ + API::Node connection() { + result = createConnection() or - exists(DataFlow::TypeTracker t2 | result = connection(t2).track(t2, t)) + result = createPool().getMember("getConnection").getParameter(0).getParameter(1) } - /** Gets a reference to a MySQL connection instance. */ - DataFlow::SourceNode connection() { result = connection(DataFlow::TypeTracker::end()) } - /** A call to the MySql `query` method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { - QueryCall() { this = [pool(), connection()].getAMethodCall("query") } + QueryCall() { + exists(API::Node recv | recv = createPool() or recv = connection() | + this = recv.getMember("query").getReturn().getAUse() + ) + } override DataFlow::Node getAQueryArgument() { result = getArgument(0) } } @@ -76,7 +63,12 @@ private module MySql { /** A call to the `escape` or `escapeId` method that performs SQL sanitization. */ class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr { EscapingSanitizer() { - this = [mysql(), pool(), connection()].getAMethodCall(["escape", "escapeId"]).asExpr() and + this = + [mysql(), createPool(), connection()] + .getMember(["escape", "escapeId"]) + .getReturn() + .getAUse() + .asExpr() and input = this.getArgument(0) and output = this } @@ -87,8 +79,9 @@ private module MySql { string kind; Credentials() { - exists(string prop | - this = [createConnection(), createPool()].getOptionArgument(0, prop).asExpr() and + exists(API::Node call, string prop | + call in [createConnection(), createPool()] and + call.getAUse().asExpr().(CallExpr).hasOptionArgument(0, prop, this) and ( prop = "user" and kind = "user name" or @@ -105,49 +98,29 @@ private module MySql { * Provides classes modelling the `pg` package. */ private module Postgres { - /** Gets an expression that constructs a new connection pool. */ - DataFlow::InvokeNode newPool() { - // new require('pg').Pool() - result = DataFlow::moduleImport("pg").getAConstructorInvocation("Pool") - or - // new require('pg-pool') - result = DataFlow::moduleImport("pg-pool").getAnInstantiation() - } + /** Gets an expression of the form `new require('pg').Client()`. */ + API::Node newClient() { result = API::moduleImport("pg").getMember("Client").getInstance() } - /** Gets a data flow node referring to a connection pool. */ - private DataFlow::SourceNode pool(DataFlow::TypeTracker t) { - t.start() and - result = newPool() + /** Gets a data flow node that holds a freshly created Postgres client instance. */ + API::Node client() { + result = newClient() or - exists(DataFlow::TypeTracker t2 | result = pool(t2).track(t2, t)) - } - - /** Gets a data flow node referring to a connection pool. */ - DataFlow::SourceNode pool() { result = pool(DataFlow::TypeTracker::end()) } - - /** Gets a creation of a Postgres client. */ - DataFlow::InvokeNode newClient() { - result = DataFlow::moduleImport("pg").getAConstructorInvocation("Client") + // pool.connect(function(err, client) { ... }) + result = newPool().getMember("connect").getParameter(0).getParameter(1) } - /** Gets a data flow node referring to a Postgres client. */ - private DataFlow::SourceNode client(DataFlow::TypeTracker t) { - t.start() and - ( - result = newClient() - or - result = pool().getAMethodCall("connect").getABoundCallbackParameter(0, 1) - ) + /** Gets an expression that constructs a new connection pool. */ + API::Node newPool() { + // new require('pg').Pool() + result = API::moduleImport("pg").getMember("Pool").getInstance() or - exists(DataFlow::TypeTracker t2 | result = client(t2).track(t2, t)) + // new require('pg-pool') + result = API::moduleImport("pg-pool").getInstance() } - /** Gets a data flow node referring to a Postgres client. */ - DataFlow::SourceNode client() { result = client(DataFlow::TypeTracker::end()) } - /** A call to the Postgres `query` method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { - QueryCall() { this = [client(), pool()].getAMethodCall("query") } + QueryCall() { this = [client(), newPool()].getMember("query").getReturn().getAUse() } override DataFlow::Node getAQueryArgument() { result = getArgument(0) } } @@ -162,10 +135,14 @@ private module Postgres { string kind; Credentials() { - exists(string prop | this = [newClient(), newPool()].getOptionArgument(0, prop).asExpr() | - prop = "user" and kind = "user name" - or - prop = "password" and kind = prop + exists(DataFlow::InvokeNode call, string prop | + call = [client(), newPool()].getAUse() and + this = call.getOptionArgument(0, prop).asExpr() and + ( + prop = "user" and kind = "user name" + or + prop = "password" and kind = prop + ) ) } @@ -178,29 +155,18 @@ private module Postgres { */ private module Sqlite { /** Gets a reference to the `sqlite3` module. */ - DataFlow::SourceNode sqlite() { - result = DataFlow::moduleImport("sqlite3") + API::Node sqlite() { + result = API::moduleImport("sqlite3") or - result = sqlite().getAMemberCall("verbose") + result = sqlite().getMember("verbose").getReturn() } /** Gets an expression that constructs a Sqlite database instance. */ - DataFlow::SourceNode newDb() { + API::Node newDb() { // new require('sqlite3').Database() - result = sqlite().getAConstructorInvocation("Database") + result = sqlite().getMember("Database").getInstance() } - /** Gets a data flow node referring to a Sqlite database instance. */ - private DataFlow::SourceNode db(DataFlow::TypeTracker t) { - t.start() and - result = newDb() - or - exists(DataFlow::TypeTracker t2 | result = db(t2).track(t2, t)) - } - - /** Gets a data flow node referring to a Sqlite database instance. */ - DataFlow::SourceNode db() { result = db(DataFlow::TypeTracker::end()) } - /** A call to a Sqlite query method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { QueryCall() { @@ -212,7 +178,7 @@ private module Sqlite { meth = "prepare" or meth = "run" | - this = db().getAMethodCall(meth) + this = newDb().getMember(meth).getReturn().getAUse() ) } @@ -230,30 +196,24 @@ private module Sqlite { */ private module MsSql { /** Gets a reference to the `mssql` module. */ - DataFlow::SourceNode mssql() { result = DataFlow::moduleImport("mssql") } - - /** Gets a data flow node referring to a request object. */ - private DataFlow::SourceNode request(DataFlow::TypeTracker t) { - t.start() and - ( - // new require('mssql').Request() - result = mssql().getAConstructorInvocation("Request") - or - // request.input(...) - result = request().getAMethodCall("input") - ) + API::Node mssql() { result = API::moduleImport("mssql") } + + /** Gets an expression that creates a request object. */ + API::Node request() { + // new require('mssql').Request() + result = mssql().getMember("Request").getInstance() or - exists(DataFlow::TypeTracker t2 | result = request(t2).track(t2, t)) + // request.input(...) + result = request().getMember("input").getReturn() } - /** Gets a data flow node referring to a request object. */ - DataFlow::SourceNode request() { result = request(DataFlow::TypeTracker::end()) } - /** A tagged template evaluated as a query. */ private class QueryTemplateExpr extends DatabaseAccess, DataFlow::ValueNode { override TaggedTemplateExpr astNode; - QueryTemplateExpr() { mssql().getAPropertyRead("query").flowsToExpr(astNode.getTag()) } + QueryTemplateExpr() { + mssql().getMember("query").getAUse() = DataFlow::valueNode(astNode.getTag()) + } override DataFlow::Node getAQueryArgument() { result = DataFlow::valueNode(astNode.getTemplate().getAnElement()) @@ -262,7 +222,7 @@ private module MsSql { /** A call to a MsSql query method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { - QueryCall() { this = request().getAMethodCall(["query", "batch"]) } + QueryCall() { this = request().getMember(["query", "batch"]).getReturn().getAUse() } override DataFlow::Node getAQueryArgument() { result = getArgument(0) } } @@ -292,9 +252,9 @@ private module MsSql { Credentials() { exists(DataFlow::InvokeNode call, string prop | ( - call = mssql().getAMemberCall("connect") + call = mssql().getMember("connect").getReturn().getAUse() or - call = mssql().getAConstructorInvocation("ConnectionPool") + call = mssql().getMember("ConnectionPool").getInstance().getAUse() ) and this = call.getOptionArgument(0, prop).asExpr() and ( @@ -313,26 +273,17 @@ private module MsSql { * Provides classes modelling the `sequelize` package. */ private module Sequelize { - /** Gets a node referring to an instance of the `Sequelize` class. */ - private DataFlow::SourceNode sequelize(DataFlow::TypeTracker t) { - t.start() and - result = DataFlow::moduleImport("sequelize").getAnInstantiation() - or - exists(DataFlow::TypeTracker t2 | result = sequelize(t2).track(t2, t)) - } + /** Gets an import of the `sequelize` module. */ + API::Node sequelize() { result = API::moduleImport("sequelize") } - /** Gets a node referring to an instance of the `Sequelize` class. */ - DataFlow::SourceNode sequelize() { result = sequelize(DataFlow::TypeTracker::end()) } + /** Gets an expression that creates an instance of the `Sequelize` class. */ + API::Node newSequelize() { result = sequelize().getInstance() } /** A call to `Sequelize.query`. */ - private class QueryCall extends DatabaseAccess, DataFlow::ValueNode { - override MethodCallExpr astNode; - - QueryCall() { this = sequelize().getAMethodCall("query") } + private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { + QueryCall() { this = newSequelize().getMember("query").getReturn().getAUse() } - override DataFlow::Node getAQueryArgument() { - result = DataFlow::valueNode(astNode.getArgument(0)) - } + override DataFlow::Node getAQueryArgument() { result = getArgument(0) } } /** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */ @@ -349,7 +300,7 @@ private module Sequelize { Credentials() { exists(NewExpr ne, string prop | - ne = sequelize().asExpr() and + ne = newSequelize().getAUse().asExpr() and ( this = ne.getArgument(1) and prop = "username" or @@ -376,69 +327,36 @@ private module Spanner { /** * Gets a node that refers to the `Spanner` class */ - DataFlow::SourceNode spanner() { + API::Node spanner() { // older versions - result = DataFlow::moduleImport("@google-cloud/spanner") + result = API::moduleImport("@google-cloud/spanner") or // newer versions - result = DataFlow::moduleMember("@google-cloud/spanner", "Spanner") - } - - /** Gets a data flow node referring to the result of `Spanner()` or `new Spanner()`. */ - private DataFlow::SourceNode spannerNew(DataFlow::TypeTracker t) { - t.start() and - result = spanner().getAnInvocation() - or - exists(DataFlow::TypeTracker t2 | result = spannerNew(t2).track(t2, t)) + result = API::moduleImport("@google-cloud/spanner").getMember("Spanner") } - /** Gets a data flow node referring to the result of `Spanner()` or `new Spanner()`. */ - DataFlow::SourceNode spannerNew() { result = spannerNew(DataFlow::TypeTracker::end()) } - - /** Gets a data flow node referring to the result of `.instance()`. */ - private DataFlow::SourceNode instance(DataFlow::TypeTracker t) { - t.start() and - result = spannerNew().getAMethodCall("instance") - or - exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t)) - } - - /** Gets a data flow node referring to the result of `.instance()`. */ - DataFlow::SourceNode instance() { result = instance(DataFlow::TypeTracker::end()) } - - /** Gets a node that refers to an instance of the `Database` class. */ - private DataFlow::SourceNode database(DataFlow::TypeTracker t) { - t.start() and - result = instance().getAMethodCall("database") - or - exists(DataFlow::TypeTracker t2 | result = database(t2).track(t2, t)) + /** + * Gets a node that refers to an instance of the `Database` class. + */ + API::Node database() { + result = + spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn() } - /** Gets a node that refers to an instance of the `Database` class. */ - DataFlow::SourceNode database() { result = database(DataFlow::TypeTracker::end()) } - - /** Gets a node that refers to an instance of the `v1.SpannerClient` class. */ - private DataFlow::SourceNode v1SpannerClient(DataFlow::TypeTracker t) { - t.start() and - result = spanner().getAPropertyRead("v1").getAPropertyRead("SpannerClient").getAnInstantiation() - or - exists(DataFlow::TypeTracker t2 | result = v1SpannerClient(t2).track(t2, t)) + /** + * Gets a node that refers to an instance of the `v1.SpannerClient` class. + */ + API::Node v1SpannerClient() { + result = spanner().getMember("v1").getMember("SpannerClient").getInstance() } - /** Gets a node that refers to an instance of the `v1.SpannerClient` class. */ - DataFlow::SourceNode v1SpannerClient() { result = v1SpannerClient(DataFlow::TypeTracker::end()) } - - /** Gets a node that refers to a transaction object. */ - private DataFlow::SourceNode transaction(DataFlow::TypeTracker t) { - t.start() and - result = database().getAMethodCall("runTransaction").getABoundCallbackParameter(0, 1) - or - exists(DataFlow::TypeTracker t2 | result = transaction(t2).track(t2, t)) + /** + * Gets a node that refers to a transaction object. + */ + API::Node transaction() { + result = database().getMember("runTransaction").getParameter(0).getParameter(1) } - /** Gets a node that refers to a transaction object. */ - DataFlow::SourceNode transaction() { result = transaction(DataFlow::TypeTracker::end()) } - /** * A call to a Spanner method that executes a SQL query. */ @@ -460,7 +378,8 @@ private module Spanner { */ class DatabaseRunCall extends SqlExecution { DatabaseRunCall() { - this = database().getAMethodCall(["run", "runPartitionedUpdate", "runStream"]) + this = + database().getMember(["run", "runPartitionedUpdate", "runStream"]).getReturn().getAUse() } } @@ -468,7 +387,9 @@ private module Spanner { * A call to `Transaction.run`, `Transaction.runStream` or `Transaction.runUpdate`. */ class TransactionRunCall extends SqlExecution { - TransactionRunCall() { this = transaction().getAMethodCall(["run", "runStream", "runUpdate"]) } + TransactionRunCall() { + this = transaction().getMember(["run", "runStream", "runUpdate"]).getReturn().getAUse() + } } /** @@ -476,7 +397,8 @@ private module Spanner { */ class ExecuteSqlCall extends SqlExecution { ExecuteSqlCall() { - this = v1SpannerClient().getAMethodCall(["executeSql", "executeStreamingSql"]) + this = + v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getReturn().getAUse() } override DataFlow::Node getAQueryArgument() { diff --git a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll index 87c01f46c7b7..c6c59ccaa4bd 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll @@ -5,6 +5,50 @@ import javascript +private predicate execApi(string mod, string fn, int cmdArg, int optionsArg, boolean shell) { + mod = "cross-spawn" and + fn = "sync" and + cmdArg = 0 and + shell = false and + optionsArg = -1 + or + mod = "execa" and + optionsArg = -1 and + ( + shell = false and + ( + fn = "node" or + fn = "shell" or + fn = "shellSync" or + fn = "stdout" or + fn = "stderr" or + fn = "sync" + ) + or + shell = true and + (fn = "command" or fn = "commandSync") + ) and + cmdArg = 0 +} + +private predicate execApi(string mod, int cmdArg, int optionsArg, boolean shell) { + shell = false and + ( + mod = "cross-spawn" and cmdArg = 0 and optionsArg = -1 + or + mod = "cross-spawn-async" and cmdArg = 0 and optionsArg = -1 + or + mod = "exec-async" and cmdArg = 0 and optionsArg = -1 + or + mod = "execa" and cmdArg = 0 and optionsArg = -1 + ) + or + shell = true and + mod = "exec" and + optionsArg = -2 and + cmdArg = 0 +} + private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode { int cmdArg; int optionsArg; // either a positive number representing the n'th argument, or a negative number representing the n'th last argument (e.g. -2 is the second last argument). @@ -12,61 +56,19 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I boolean sync; SystemCommandExecutors() { - exists(string mod, DataFlow::SourceNode callee | - exists(string method | - mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false and optionsArg = -1 - or - mod = "execa" and - optionsArg = -1 and - ( - shell = false and - ( - method = "shell" or - method = "shellSync" or - method = "stdout" or - method = "stderr" or - method = "sync" - ) - or - shell = true and - (method = "command" or method = "commandSync") - ) and - cmdArg = 0 - or - mod = "execa" and - method = "node" and - cmdArg = 0 and - optionsArg = 1 and - shell = false - | - callee = DataFlow::moduleMember(mod, method) and - sync = getSync(method) + exists(string mod | + exists(string fn | + execApi(mod, fn, cmdArg, optionsArg, shell) and + sync = getSync(fn) and + this = API::moduleImport(mod).getMember(fn).getReturn().getAUse() ) or + execApi(mod, cmdArg, optionsArg, shell) and sync = false and - ( - shell = false and - ( - mod = "cross-spawn" and cmdArg = 0 and optionsArg = -1 - or - mod = "cross-spawn-async" and cmdArg = 0 and optionsArg = -1 - or - mod = "exec-async" and cmdArg = 0 and optionsArg = -1 - or - mod = "execa" and cmdArg = 0 and optionsArg = -1 - ) - or - shell = true and - mod = "exec" and - optionsArg = -2 and - cmdArg = 0 - ) and - callee = DataFlow::moduleImport(mod) - | - this = callee.getACall() + this = API::moduleImport(mod).getReturn().getAUse() ) or - this = DataFlow::moduleImport("foreground-child").getACall() and + this = API::moduleImport("foreground-child").getReturn().getAUse() and cmdArg = 0 and optionsArg = 1 and shell = false and @@ -110,19 +112,19 @@ private class RemoteCommandExecutor extends SystemCommandExecution, DataFlow::In int cmdArg; RemoteCommandExecutor() { - this = DataFlow::moduleImport("remote-exec").getACall() and + this = API::moduleImport("remote-exec").getReturn().getAUse() and cmdArg = 1 or - exists(DataFlow::SourceNode ssh2, DataFlow::SourceNode client | - ssh2 = DataFlow::moduleImport("ssh2") and - (client = ssh2 or client = ssh2.getAPropertyRead("Client")) and - this = client.getAnInstantiation().getAMethodCall("exec") and + exists(API::Node ssh2, API::Node client | + ssh2 = API::moduleImport("ssh2") and + client in [ssh2, ssh2.getMember("Client")] and + this = client.getInstance().getMember("exec").getReturn().getAUse() and cmdArg = 0 ) or - exists(DataFlow::SourceNode ssh2stream | - ssh2stream = DataFlow::moduleMember("ssh2-streams", "SSH2Stream") and - this = ssh2stream.getAnInstantiation().getAMethodCall("exec") and + exists(API::Node ssh2stream | + ssh2stream = API::moduleImport("ssh2-streams").getMember("SSH2Stream") and + this = ssh2stream.getInstance().getMember("exec").getReturn().getAUse() and cmdArg = 1 ) } @@ -137,7 +139,7 @@ private class RemoteCommandExecutor extends SystemCommandExecution, DataFlow::In } private class Opener extends SystemCommandExecution, DataFlow::InvokeNode { - Opener() { this = DataFlow::moduleImport("opener").getACall() } + Opener() { this = API::moduleImport("opener").getReturn().getAUse() } override DataFlow::Node getACommandArgument() { result = getOptionArgument(1, "command") } diff --git a/javascript/ql/src/semmle/javascript/security/dataflow/MissingRateLimiting.qll b/javascript/ql/src/semmle/javascript/security/dataflow/MissingRateLimiting.qll index 82db9e0fad31..9b26660c6f0e 100644 --- a/javascript/ql/src/semmle/javascript/security/dataflow/MissingRateLimiting.qll +++ b/javascript/ql/src/semmle/javascript/security/dataflow/MissingRateLimiting.qll @@ -126,7 +126,7 @@ abstract class RateLimiter extends Express::RouteHandlerExpr { } */ class ExpressRateLimit extends RateLimiter { ExpressRateLimit() { - DataFlow::moduleImport("express-rate-limit").getAnInvocation().flowsToExpr(this) + this = API::moduleImport("express-rate-limit").getReturn().getAUse().asExpr() } } @@ -135,11 +135,7 @@ class ExpressRateLimit extends RateLimiter { */ class BruteForceRateLimit extends RateLimiter { BruteForceRateLimit() { - exists(DataFlow::ModuleImportNode expressBrute, DataFlow::SourceNode prevent | - expressBrute.getPath() = "express-brute" and - prevent = expressBrute.getAnInstantiation().getAPropertyRead("prevent") and - prevent.flowsToExpr(this) - ) + this = API::moduleImport("express-brute").getInstance().getMember("prevent").getAUse().asExpr() } } @@ -148,11 +144,8 @@ class BruteForceRateLimit extends RateLimiter { */ class RouteHandlerLimitedByExpressLimiter extends RateLimitedRouteHandlerExpr { RouteHandlerLimitedByExpressLimiter() { - exists(DataFlow::ModuleImportNode expressLimiter | - expressLimiter.getPath() = "express-limiter" and - expressLimiter.getACall().getArgument(0).getALocalSource().asExpr() = - this.getSetup().getRouter() - ) + API::moduleImport("express-limiter").getParameter(0).getARhs().getALocalSource().asExpr() = + this.getSetup().getRouter() } } @@ -175,14 +168,14 @@ class RouteHandlerLimitedByExpressLimiter extends RateLimitedRouteHandlerExpr { class RateLimiterFlexibleRateLimiter extends DataFlow::FunctionNode { RateLimiterFlexibleRateLimiter() { exists( - string rateLimiterClassName, DataFlow::SourceNode rateLimiterClass, - DataFlow::SourceNode rateLimiterInstance, DataFlow::ParameterNode request + string rateLimiterClassName, API::Node rateLimiterClass, API::Node rateLimiterConsume, + DataFlow::ParameterNode request | rateLimiterClassName.matches("RateLimiter%") and - rateLimiterClass = DataFlow::moduleMember("rate-limiter-flexible", rateLimiterClassName) and - rateLimiterInstance = rateLimiterClass.getAnInstantiation() and + rateLimiterClass = API::moduleImport("rate-limiter-flexible").getMember(rateLimiterClassName) and + rateLimiterConsume = rateLimiterClass.getInstance().getMember("consume") and request.getParameter() = getRouteHandlerParameter(getFunction(), "request") and - request.getAPropertyRead() = rateLimiterInstance.getAMemberCall("consume").getAnArgument() + request.getAPropertyRead().flowsTo(rateLimiterConsume.getAParameter().getARhs()) ) } } diff --git a/javascript/ql/test/ApiGraphs/VerifyAssertions.qll b/javascript/ql/test/ApiGraphs/VerifyAssertions.qll new file mode 100644 index 000000000000..4c92792cce5e --- /dev/null +++ b/javascript/ql/test/ApiGraphs/VerifyAssertions.qll @@ -0,0 +1,117 @@ +/** + * A test query that verifies assertions about the API graph embedded in source-code comments. + * + * An assertion is a comment of the form `def ` or `use `, and asserts that + * there is a def/use feature reachable from the root along the given path (described using + * s-expression syntax), and its associated data-flow node must start on the same line as the + * comment. + * + * We also support negative assertions of the form `!def ` or `!use `, which assert + * that there _isn't_ a node with the given path on the same line. + * + * The query only produces output for failed assertions, meaning that it should have no output + * under normal circumstances. + * + * Note that this query file isn't itself meant to be run as a test; instead, the `.qlref`s + * referring to it from inside the individual test directories should be run. However, when + * all tests are run this test will also be run, hence we need to check in a (somewhat nonsensical) + * `.expected` file for it as well. + */ + +import javascript + +private DataFlow::Node getNode(API::Node nd, string kind) { + kind = "def" and + result = nd.getARhs() + or + kind = "use" and + result = nd.getAUse() +} + +private string getLoc(DataFlow::Node nd) { + exists(string filepath, int startline | + nd.hasLocationInfo(filepath, startline, _, _, _) and + result = filepath + ":" + startline + ) +} + +/** + * An assertion matching a data-flow node against an API-graph feature. + */ +class Assertion extends Comment { + string polarity; + string expectedKind; + string expectedLoc; + + Assertion() { + exists(string txt, string rex | + txt = this.getText().trim() and + rex = "(!?)(def|use) .*" + | + polarity = txt.regexpCapture(rex, 1) and + expectedKind = txt.regexpCapture(rex, 2) and + expectedLoc = getFile().getAbsolutePath() + ":" + getLocation().getStartLine() + ) + } + + string getEdgeLabel(int i) { result = this.getText().regexpFind("(?<=\\()[^()]+", i, _).trim() } + + int getPathLength() { result = max(int i | exists(getEdgeLabel(i))) + 1 } + + API::Node lookup(int i) { + i = getPathLength() and + result = API::root() + or + result = lookup(i + 1).getASuccessor(getEdgeLabel(i)) + } + + predicate isNegative() { polarity = "!" } + + predicate holds() { getLoc(getNode(lookup(0), expectedKind)) = expectedLoc } + + string tryExplainFailure() { + exists(int i, API::Node nd, string prefix, string suffix | + nd = lookup(i) and + i > 0 and + not exists(lookup([0 .. i - 1])) and + prefix = nd + " has no outgoing edge labelled " + getEdgeLabel(i - 1) + ";" and + if exists(nd.getASuccessor()) + then + suffix = + "it does have outgoing edges labelled " + + concat(string lbl | exists(nd.getASuccessor(lbl)) | lbl, ", ") + "." + else suffix = "it has no outgoing edges at all." + | + result = prefix + " " + suffix + ) + or + exists(API::Node nd, string kind | nd = lookup(0) | + exists(getNode(nd, kind)) and + not exists(getNode(nd, expectedKind)) and + result = "Expected " + expectedKind + " node, but found " + kind + " node." + ) + or + exists(DataFlow::Node nd | nd = getNode(lookup(0), expectedKind) | + not getLoc(nd) = expectedLoc and + result = "Node not found on this line (but there is one on line " + min(getLoc(nd)) + ")." + ) + } + + string explainFailure() { + if isNegative() + then ( + holds() and + result = "Negative assertion failed." + ) else ( + not holds() and + ( + result = tryExplainFailure() + or + not exists(tryExplainFailure()) and + result = "Positive assertion failed for unknown reasons." + ) + ) + } +} + +query predicate failed(Assertion a, string explanation) { explanation = a.explainFailure() } diff --git a/javascript/ql/test/ApiGraphs/argprops/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/argprops/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/argprops/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/argprops/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/argprops/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/argprops/index.js b/javascript/ql/test/ApiGraphs/argprops/index.js new file mode 100644 index 000000000000..8f7169081dc8 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/argprops/index.js @@ -0,0 +1,6 @@ +const assert = require("assert"); + +let o = { + foo: 23 /* def (member foo (parameter 0 (member equal (member exports (module assert))))) */ +}; +assert.equal(o, o); diff --git a/javascript/ql/test/ApiGraphs/argprops/package.json b/javascript/ql/test/ApiGraphs/argprops/package.json new file mode 100644 index 000000000000..2a21d6294b3e --- /dev/null +++ b/javascript/ql/test/ApiGraphs/argprops/package.json @@ -0,0 +1,3 @@ +{ + "name": "argprops" +} diff --git a/javascript/ql/test/ApiGraphs/async-await/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/async-await/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/async-await/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/async-await/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/async-await/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/async-await/index.js b/javascript/ql/test/ApiGraphs/async-await/index.js new file mode 100644 index 000000000000..0c3b43461c1e --- /dev/null +++ b/javascript/ql/test/ApiGraphs/async-await/index.js @@ -0,0 +1,5 @@ +const fs = require('fs-extra'); + +module.exports.foo = async function foo() { + return await fs.copy('/tmp/myfile', '/tmp/mynewfile'); /* use (promised (return (member copy (member exports (module fs-extra))))) */ /* def (promised (return (member foo (member exports (module async-await))))) */ +}; diff --git a/javascript/ql/test/ApiGraphs/async-await/package.json b/javascript/ql/test/ApiGraphs/async-await/package.json new file mode 100644 index 000000000000..f4abf1711545 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/async-await/package.json @@ -0,0 +1,6 @@ +{ + "name": "async-await", + "dependencies": { + "fs-extra": "*" + } +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/branching-flow/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/branching-flow/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/branching-flow/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/branching-flow/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/branching-flow/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/branching-flow/index.js b/javascript/ql/test/ApiGraphs/branching-flow/index.js new file mode 100644 index 000000000000..8af9a7ac5864 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/branching-flow/index.js @@ -0,0 +1,7 @@ +const fs = require('fs'); + +exports.foo = function (cb) { + if (!cb) + cb = function () { }; + cb(fs.readFileSync("/etc/passwd")); /* def (parameter 0 (parameter 0 (member foo (member exports (module branching-flow))))) */ +}; \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/branching-flow/package.json b/javascript/ql/test/ApiGraphs/branching-flow/package.json new file mode 100644 index 000000000000..7f366c52ed4c --- /dev/null +++ b/javascript/ql/test/ApiGraphs/branching-flow/package.json @@ -0,0 +1,3 @@ +{ + "name": "branching-flow" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/classes/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/classes/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/classes/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/classes/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/classes/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/classes/classes.js b/javascript/ql/test/ApiGraphs/classes/classes.js new file mode 100644 index 000000000000..4d6fab7ee373 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/classes/classes.js @@ -0,0 +1,23 @@ +const util = require('util'); +const EventEmitter = require('events'); + +function MyStream() { + EventEmitter.call(this); +} + +util.inherits(MyStream, EventEmitter); + +MyStream.prototype.write = (data) => this.emit('data', data); + +function MyOtherStream() { /* use (instance (member MyOtherStream (member exports (module classes)))) */ + EventEmitter.call(this); +} + +util.inherits(MyOtherStream, EventEmitter); + +MyOtherStream.prototype.write = function (data) { /* use (instance (member MyOtherStream (member exports (module classes)))) */ + this.emit('data', data); + return this; +}; + +module.exports.MyOtherStream = MyOtherStream; diff --git a/javascript/ql/test/ApiGraphs/classes/package.json b/javascript/ql/test/ApiGraphs/classes/package.json new file mode 100644 index 000000000000..b237b8db8fe9 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/classes/package.json @@ -0,0 +1,4 @@ +{ + "name": "classes", + "main": "./classes.js" +} diff --git a/javascript/ql/test/ApiGraphs/ctor-arg/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/ctor-arg/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/ctor-arg/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/ctor-arg/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/ctor-arg/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/ctor-arg/index.js b/javascript/ql/test/ApiGraphs/ctor-arg/index.js new file mode 100644 index 000000000000..24f9f0d938e8 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/ctor-arg/index.js @@ -0,0 +1,5 @@ +export class A { + constructor(x) { /* use (parameter 0 (member A (member exports (module ctor-arg)))) */ + console.log(x); + } +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/ctor-arg/package.json b/javascript/ql/test/ApiGraphs/ctor-arg/package.json new file mode 100644 index 000000000000..9cf34139f0af --- /dev/null +++ b/javascript/ql/test/ApiGraphs/ctor-arg/package.json @@ -0,0 +1,3 @@ +{ + "name": "ctor-arg" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/custom-entry-point/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/custom-entry-point/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/custom-entry-point/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/custom-entry-point/VerifyAssertions.ql new file mode 100644 index 000000000000..fb8a943f2cad --- /dev/null +++ b/javascript/ql/test/ApiGraphs/custom-entry-point/VerifyAssertions.ql @@ -0,0 +1,9 @@ +class CustomEntryPoint extends API::EntryPoint { + CustomEntryPoint() { this = "CustomEntryPoint" } + + override DataFlow::SourceNode getAUse() { result = DataFlow::globalVarRef("CustomEntryPoint") } + + override DataFlow::Node getARhs() { none() } +} + +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/custom-entry-point/index.js b/javascript/ql/test/ApiGraphs/custom-entry-point/index.js new file mode 100644 index 000000000000..43819d6ef6b2 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/custom-entry-point/index.js @@ -0,0 +1 @@ +module.exports = CustomEntryPoint.foo; /* use (member foo (CustomEntryPoint)) */ diff --git a/javascript/ql/test/ApiGraphs/custom-entry-point/package.json b/javascript/ql/test/ApiGraphs/custom-entry-point/package.json new file mode 100644 index 000000000000..902d9f087d36 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/custom-entry-point/package.json @@ -0,0 +1,3 @@ +{ + "name": "custom-entry-point" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/cyclic/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/cyclic/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/cyclic/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/cyclic/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/cyclic/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/cyclic/index.js b/javascript/ql/test/ApiGraphs/cyclic/index.js new file mode 100644 index 000000000000..81bea276eb89 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/cyclic/index.js @@ -0,0 +1,4 @@ +const foo = require("foo"); + +while(foo) + foo = foo.foo; /* use (member foo (member exports (module foo))) */ /* use (member foo (member foo (member exports (module foo)))) */ diff --git a/javascript/ql/test/ApiGraphs/cyclic/package.json b/javascript/ql/test/ApiGraphs/cyclic/package.json new file mode 100644 index 000000000000..ebcde915f44e --- /dev/null +++ b/javascript/ql/test/ApiGraphs/cyclic/package.json @@ -0,0 +1,6 @@ +{ + "name": "cyclic", + "dependencies": { + "foo": "*" + } +} diff --git a/javascript/ql/test/ApiGraphs/dynamic-prop-read/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/dynamic-prop-read/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/dynamic-prop-read/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/dynamic-prop-read/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/dynamic-prop-read/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/dynamic-prop-read/index.js b/javascript/ql/test/ApiGraphs/dynamic-prop-read/index.js new file mode 100644 index 000000000000..2c76a4091c96 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/dynamic-prop-read/index.js @@ -0,0 +1,5 @@ +const MyStream = require('classes').MyStream; + +var s = new MyStream(); +for (let m of ["write"]) + s[m]("Hello, world!"); /* use (member * (instance (member MyStream (member exports (module classes))))) */ \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/dynamic-prop-read/package.json b/javascript/ql/test/ApiGraphs/dynamic-prop-read/package.json new file mode 100644 index 000000000000..2c83b5bf0d79 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/dynamic-prop-read/package.json @@ -0,0 +1,6 @@ +{ + "name": "dynamic-prop-read", + "dependencies": { + "classes": "*" + } +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/imprecise-export/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/imprecise-export/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/imprecise-export/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/imprecise-export/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/imprecise-export/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/imprecise-export/index.js b/javascript/ql/test/ApiGraphs/imprecise-export/index.js new file mode 100644 index 000000000000..a40722bb06a2 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/imprecise-export/index.js @@ -0,0 +1,3 @@ +anotherUnknownFunction().foo = 42; /* !def (member foo (member exports (module imprecise-export))) */ + +module.exports = unknownFunction(); diff --git a/javascript/ql/test/ApiGraphs/imprecise-export/package.json b/javascript/ql/test/ApiGraphs/imprecise-export/package.json new file mode 100644 index 000000000000..7554b8ae1169 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/imprecise-export/package.json @@ -0,0 +1,3 @@ +{ + "name": "imprecise-export" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/imprecision/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/imprecision/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/imprecision/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/imprecision/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/imprecision/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/imprecision/index.js b/javascript/ql/test/ApiGraphs/imprecision/index.js new file mode 100644 index 000000000000..e8270e8d7e04 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/imprecision/index.js @@ -0,0 +1,11 @@ +const http = require('http'); +let req = http.get(url, cb); +req.on('connect', ( + req, /* use (parameter 0 (parameter 1 (member on (return (member get (member exports (module http))))))) */ + clientSocket, head) => { /* ... */ }); +req.on('information', ( + info /* use (parameter 0 (parameter 1 (member on (return (member get (member exports (module http))))))) */ + ) => { /* ... */ }); + +req.on('connect', () => { }) /* def (parameter 0 (member on (return (member get (member exports (module http)))))) */ + .on('information', () => { }) /* def (parameter 0 (member on (return (member on (return (member get (member exports (module http)))))))) */; \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/imprecision/package.json b/javascript/ql/test/ApiGraphs/imprecision/package.json new file mode 100644 index 000000000000..586715f89274 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/imprecision/package.json @@ -0,0 +1,3 @@ +{ + "name": "imprecision" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/namespaced-package/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/namespaced-package/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/namespaced-package/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/namespaced-package/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/namespaced-package/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/namespaced-package/index.js b/javascript/ql/test/ApiGraphs/namespaced-package/index.js new file mode 100644 index 000000000000..4b3b20eac1d4 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/namespaced-package/index.js @@ -0,0 +1,2 @@ +import foo from "@myorg/myotherpkg"; +foo(); /* use (member default (member exports (module @myorg/myotherpkg))) */ diff --git a/javascript/ql/test/ApiGraphs/namespaced-package/package.json b/javascript/ql/test/ApiGraphs/namespaced-package/package.json new file mode 100644 index 000000000000..70a3c46e6ec2 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/namespaced-package/package.json @@ -0,0 +1,6 @@ +{ + "name": "@myorg/mypkg", + "dependencies": { + "@myorg/myotherpkg": "*" + } +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/nested-property-export/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/nested-property-export/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/nested-property-export/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/nested-property-export/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/nested-property-export/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/nested-property-export/index.js b/javascript/ql/test/ApiGraphs/nested-property-export/index.js new file mode 100644 index 000000000000..224f16295b48 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/nested-property-export/index.js @@ -0,0 +1,7 @@ +module.exports.foo = function (x) { /* use (parameter 0 (member foo (member exports (module nested-property-export)))) */ + return x; +}; + +module.exports.foo.bar = function (y) { /* use (parameter 0 (member bar (member foo (member exports (module nested-property-export))))) */ + return y; +}; diff --git a/javascript/ql/test/ApiGraphs/nested-property-export/package.json b/javascript/ql/test/ApiGraphs/nested-property-export/package.json new file mode 100644 index 000000000000..57fc1e3e231b --- /dev/null +++ b/javascript/ql/test/ApiGraphs/nested-property-export/package.json @@ -0,0 +1,3 @@ +{ + "name": "nested-property-export" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/nonlocal/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/nonlocal/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/nonlocal/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/nonlocal/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/nonlocal/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/nonlocal/index.js b/javascript/ql/test/ApiGraphs/nonlocal/index.js new file mode 100644 index 000000000000..debbd554b90e --- /dev/null +++ b/javascript/ql/test/ApiGraphs/nonlocal/index.js @@ -0,0 +1,15 @@ +const express = require('express'); + +var app1 = new express(); +app1.get('/', + (req, res) => res.send('Hello World!') /* def (parameter 1 (member get (instance (member exports (module express))))) */ +); + +function makeApp() { + return new express(); +} + +var app2 = makeApp(); +app2.get('/', + (req, res) => res.send('Hello World!') /* def (parameter 1 (member get (instance (member exports (module express))))) */ +); \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/nonlocal/package.json b/javascript/ql/test/ApiGraphs/nonlocal/package.json new file mode 100644 index 000000000000..9ac64d76c1f5 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/nonlocal/package.json @@ -0,0 +1,6 @@ +{ + "name": "nonlocal", + "dependencies": { + "express": "*" + } +} diff --git a/javascript/ql/test/ApiGraphs/promises/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/promises/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/promises/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/promises/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/promises/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/promises/index.js b/javascript/ql/test/ApiGraphs/promises/index.js new file mode 100644 index 000000000000..a3552dbdf643 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/promises/index.js @@ -0,0 +1,21 @@ +const fs = require("fs"), + fse = require("fs-extra"), + base64 = require("base-64"); + +module.exports.readFile = function (f) { + return new Promise((res, rej) => { + fs.readFile(f, (err, data) => { + if (err) + rej(err); + else + res(data); /* def (promised (return (member readFile (member exports (module promises))))) */ + }); + }); +}; + +module.exports.readFileAndEncode = function (f) { + return fse.readFile(f) + .then((data) => /* use (promised (return (member readFile (member exports (module fs-extra))))) */ + base64.encode(data) /* def (promised (return (member readFileAndEncode (member exports (module promises))))) */ + ); +}; \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/promises/package.json b/javascript/ql/test/ApiGraphs/promises/package.json new file mode 100644 index 000000000000..992975b2a675 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/promises/package.json @@ -0,0 +1,6 @@ +{ + "name": "promises", + "dependencies": { + "fs-extra": "*" + } +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/reexport/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/reexport/VerifyAssertions.expected new file mode 100644 index 000000000000..9815f2e44c84 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/VerifyAssertions.expected @@ -0,0 +1 @@ +| lib/utils.js:1:38:1:120 | /* use ... )))) */ | def (member util (member exports (module reexport))) has no outgoing edge labelled member id; it has no outgoing edges at all. | diff --git a/javascript/ql/test/ApiGraphs/reexport/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/reexport/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/reexport/index.js b/javascript/ql/test/ApiGraphs/reexport/index.js new file mode 100644 index 000000000000..b88845d7175d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/index.js @@ -0,0 +1,6 @@ +const impl = require("./lib/impl.js"); + +module.exports = { + impl, + util: require("./lib/utils") +}; \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/reexport/lib/impl.js b/javascript/ql/test/ApiGraphs/reexport/lib/impl.js new file mode 100644 index 000000000000..5c65e1cb6363 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/lib/impl.js @@ -0,0 +1,3 @@ +module.exports = function () { + return 42; /* def (return (member impl (member exports (module reexport)))) */ +}; diff --git a/javascript/ql/test/ApiGraphs/reexport/lib/utils.js b/javascript/ql/test/ApiGraphs/reexport/lib/utils.js new file mode 100644 index 000000000000..35110d694203 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/lib/utils.js @@ -0,0 +1,3 @@ +module.exports.id = function id(x) { /* use (parameter 0 (member id (member util (member exports (module reexport)))) */ + return x; +}; diff --git a/javascript/ql/test/ApiGraphs/reexport/package.json b/javascript/ql/test/ApiGraphs/reexport/package.json new file mode 100644 index 000000000000..b33f2e04bf8e --- /dev/null +++ b/javascript/ql/test/ApiGraphs/reexport/package.json @@ -0,0 +1,3 @@ +{ + "name": "reexport" +} diff --git a/javascript/ql/test/ApiGraphs/return-self/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/return-self/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/return-self/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/return-self/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/return-self/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/return-self/index.js b/javascript/ql/test/ApiGraphs/return-self/index.js new file mode 100644 index 000000000000..229adadfa0ae --- /dev/null +++ b/javascript/ql/test/ApiGraphs/return-self/index.js @@ -0,0 +1,6 @@ +export class A { + foo() { + return this; /* def (return (member foo (instance (member A (member exports (module return-self)))))) */ + } + bar(x) { } /* use (parameter 0 (member bar (instance (member A (member exports (module return-self)))))) */ +} diff --git a/javascript/ql/test/ApiGraphs/return-self/package.json b/javascript/ql/test/ApiGraphs/return-self/package.json new file mode 100644 index 000000000000..807cd16edd6a --- /dev/null +++ b/javascript/ql/test/ApiGraphs/return-self/package.json @@ -0,0 +1,3 @@ +{ + "name": "return-self" +} \ No newline at end of file diff --git a/javascript/ql/test/ApiGraphs/typed/VerifyAssertions.expected b/javascript/ql/test/ApiGraphs/typed/VerifyAssertions.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/ApiGraphs/typed/VerifyAssertions.ql b/javascript/ql/test/ApiGraphs/typed/VerifyAssertions.ql new file mode 100644 index 000000000000..b9c54e26072d --- /dev/null +++ b/javascript/ql/test/ApiGraphs/typed/VerifyAssertions.ql @@ -0,0 +1 @@ +import ApiGraphs.VerifyAssertions diff --git a/javascript/ql/test/ApiGraphs/typed/index.ts b/javascript/ql/test/ApiGraphs/typed/index.ts new file mode 100644 index 000000000000..b89dac43aff1 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/typed/index.ts @@ -0,0 +1,24 @@ +import * as mongodb from "mongodb"; + +const express = require("express") as any; +const bodyParser = require("body-parser") as any; + +declare function getCollection(): mongodb.Collection; + +let app = express(); + +app.use(bodyParser.json()); + +app.post("/find", (req, res) => { + let v = JSON.parse(req.body.x); + getCollection().find({ id: v }); /* use (member find (instance (member Collection (module mongodb)))) */ +}); + +import * as mongoose from "mongoose"; +declare function getMongooseModel(): mongoose.Model; +declare function getMongooseQuery(): mongoose.Query; +app.post("/find", (req, res) => { + let v = JSON.parse(req.body.x); + getMongooseModel().find({ id: v }); /* def (parameter 0 (member find (instance (member Model (module mongoose))))) */ + getMongooseQuery().find({ id: v }); /* def (parameter 0 (member find (instance (member Query (module mongoose))))) */ +}); diff --git a/javascript/ql/test/ApiGraphs/typed/package.json b/javascript/ql/test/ApiGraphs/typed/package.json new file mode 100644 index 000000000000..5862feda88fe --- /dev/null +++ b/javascript/ql/test/ApiGraphs/typed/package.json @@ -0,0 +1,7 @@ +{ + "name": "typed", + "dependencies": { + "mongodb": "*", + "mongoose": "*" + } +} diff --git a/javascript/ql/test/ApiGraphs/typed/shim.d.ts b/javascript/ql/test/ApiGraphs/typed/shim.d.ts new file mode 100644 index 000000000000..2f3b2ce9b512 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/typed/shim.d.ts @@ -0,0 +1,13 @@ +declare module "mongodb" { + interface Collection { + find(query: any): any; + } +} +declare module "mongoose" { + interface Model { + find(query: any): any; + } + interface Query { + find(query: any): any; + } +} diff --git a/javascript/ql/test/ApiGraphs/typed/tsconfig.json b/javascript/ql/test/ApiGraphs/typed/tsconfig.json new file mode 100644 index 000000000000..4c06d765a041 --- /dev/null +++ b/javascript/ql/test/ApiGraphs/typed/tsconfig.json @@ -0,0 +1,6 @@ +{ + "include": ["."], + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected b/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected index c6f466cdae91..a7d4af58a909 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected @@ -4,6 +4,8 @@ | mssql3.js:12:13:12:22 | 'password' | password | | mysql1.js:6:14:6:17 | 'me' | user name | | mysql1.js:7:14:7:21 | 'secret' | password | +| mysql1a.js:10:9:10:12 | 'me' | user name | +| mysql1a.js:11:13:11:20 | 'secret' | password | | mysql2.js:7:21:7:25 | 'bob' | user name | | mysql2.js:8:21:8:28 | 'secret' | password | | mysql2tst.js:8:9:8:14 | 'root' | user name | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index fb8a133937d6..b3e9e240944f 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -5,6 +5,7 @@ | mssql2.js:22:24:22:43 | 'select 1 as number' | | mysql1.js:13:18:13:43 | 'SELECT ... lution' | | mysql1.js:18:18:22:1 | {\\n s ... vid']\\n} | +| mysql1a.js:17:18:17:43 | 'SELECT ... lution' | | mysql2.js:12:12:12:37 | 'SELECT ... lution' | | mysql2tst.js:14:3:14:62 | 'SELECT ... ` > 45' | | mysql2tst.js:23:3:23:56 | 'SELECT ... e` > ?' | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mysql1a.js b/javascript/ql/test/library-tests/frameworks/SQL/mysql1a.js new file mode 100644 index 000000000000..eff9c35101e8 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/mysql1a.js @@ -0,0 +1,24 @@ +// Adapted from the documentation of https://github.com/mysqljs/mysql, +// which is licensed under the MIT license; see file mysqljs-License. + +function importMySql() { + return require("mysql"); +} + +var connection = importMySql().createConnection({ + host: 'localhost', + user: 'me', + password: 'secret', + database: 'my_db' +}); + +connection.connect(); + +connection.query('SELECT 1 + 1 AS solution', function (error, results, fields) { + if (error) throw error; + console.log('The solution is: ', results[0].solution); +}); + +connection.end(); + +exports.connection = connection; diff --git a/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/DeadStoreOfProperty.expected b/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/DeadStoreOfProperty.expected index 68890cc34da7..b22ab1e4b570 100644 --- a/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/DeadStoreOfProperty.expected +++ b/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/DeadStoreOfProperty.expected @@ -1,3 +1,4 @@ +| exports.js:2:1:2:22 | exports ... = "yes" | This write to property 'answer' is useless, since $@ always overrides it. | exports.js:3:1:3:21 | exports ... = "no" | another property write | | fieldInit.ts:10:3:10:8 | f = 4; | This write to property 'f' is useless, since $@ always overrides it. | fieldInit.ts:13:5:13:14 | this.f = 5 | another property write | | real-world-examples.js:5:4:5:11 | o.p = 42 | This write to property 'p' is useless, since $@ always overrides it. | real-world-examples.js:10:2:10:9 | o.p = 42 | another property write | | real-world-examples.js:15:9:15:18 | o.p1 += 42 | This write to property 'p1' is useless, since $@ always overrides it. | real-world-examples.js:15:2:15:18 | o.p1 = o.p1 += 42 | another property write | diff --git a/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/exports.js b/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/exports.js new file mode 100644 index 000000000000..c4b70604781d --- /dev/null +++ b/javascript/ql/test/query-tests/Declarations/DeadStoreOfProperty/exports.js @@ -0,0 +1,3 @@ +var exports = module.exports; +exports.answer = "yes"; // NOT OK +exports.answer = "no"; diff --git a/javascript/ql/test/query-tests/Security/CWE-770/rateLimit.ts b/javascript/ql/test/query-tests/Security/CWE-770/rateLimit.ts new file mode 100644 index 000000000000..e9f8854b8ccf --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-770/rateLimit.ts @@ -0,0 +1,5 @@ +import rateLimit from 'express-rate-limit'; + +const rateLimitMiddleware = rateLimit(); + +export default rateLimitMiddleware; diff --git a/javascript/ql/test/query-tests/Security/CWE-770/tst2.ts b/javascript/ql/test/query-tests/Security/CWE-770/tst2.ts new file mode 100644 index 000000000000..32e04e2ff206 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-770/tst2.ts @@ -0,0 +1,8 @@ +import express from 'express'; +import rateLimiter from './rateLimit'; + +const app = express(); +app.use(rateLimiter); +app.get('/', (req, res) => { + res.sendFile('index.html'); // OK +});