diff --git a/javascript/ql/lib/semmle/javascript/ApiGraphs.qll b/javascript/ql/lib/semmle/javascript/ApiGraphs.qll index d2229795642f..0f8dd3178d7b 100644 --- a/javascript/ql/lib/semmle/javascript/ApiGraphs.qll +++ b/javascript/ql/lib/semmle/javascript/ApiGraphs.qll @@ -11,6 +11,7 @@ import javascript private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps +private import internal.CachedStages /** * Provides classes and predicates for working with APIs defined or used in a database. @@ -33,6 +34,7 @@ module API { * As another example, in the assignment `exports.plusOne = (x) => x+1` the two references to * `x` are uses of the first parameter of `plusOne`. */ + pragma[inline] DataFlow::Node getAUse() { exists(DataFlow::SourceNode src | Impl::use(this, src) | Impl::trackUseNode(src).flowsTo(result) @@ -105,22 +107,31 @@ module API { * 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 = this.getASuccessor(Label::member(m)) } + cached + Node getMember(string m) { + Stages::APIStage::ref() and + result = this.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 = this.getASuccessor(Label::unknownMember()) } + cached + Node getUnknownMember() { + Stages::APIStage::ref() and + result = this.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. */ + cached Node getAMember() { - result = this.getASuccessor(Label::member(_)) or + Stages::APIStage::ref() and + result = this.getMember(_) + or result = this.getUnknownMember() } @@ -135,7 +146,11 @@ module API { * This predicate may have multiple results when there are multiple constructor calls invoking this API component. * Consider using `getAnInstantiation()` if there is a need to distinguish between individual constructor calls. */ - Node getInstance() { result = this.getASuccessor(Label::instance()) } + cached + Node getInstance() { + Stages::APIStage::ref() and + result = this.getASuccessor(Label::instance()) + } /** * Gets a node representing the `i`th parameter of the function represented by this node. @@ -143,16 +158,16 @@ module API { * This predicate may have multiple results when there are multiple invocations of this API component. * Consider using `getAnInvocation()` if there is a need to distingiush between individual calls. */ - bindingset[i] - Node getParameter(int i) { result = this.getASuccessor(Label::parameter(i)) } + cached + Node getParameter(int i) { + Stages::APIStage::ref() and + result = this.getASuccessor(Label::parameter(i)) + } /** * Gets the number of parameters of the function represented by this node. */ - int getNumParameter() { - result = - max(string s | exists(this.getASuccessor(Label::parameterByStringIndex(s))) | s.toInt()) + 1 - } + int getNumParameter() { result = max(int s | exists(this.getParameter(s))) + 1 } /** * Gets a node representing the last parameter of the function represented by this node. @@ -165,7 +180,11 @@ module API { /** * Gets a node representing the receiver of the function represented by this node. */ - Node getReceiver() { result = this.getASuccessor(Label::receiver()) } + cached + Node getReceiver() { + Stages::APIStage::ref() and + result = this.getASuccessor(Label::receiver()) + } /** * Gets a node representing a parameter or the receiver of the function represented by this @@ -175,8 +194,11 @@ module API { * there are multiple invocations of this API component. * Consider using `getAnInvocation()` if there is a need to distingiush between individual calls. */ + cached Node getAParameter() { - result = this.getASuccessor(Label::parameterByStringIndex(_)) or + Stages::APIStage::ref() and + result = this.getParameter(_) + or result = this.getReceiver() } @@ -186,18 +208,30 @@ module API { * This predicate may have multiple results when there are multiple invocations of this API component. * Consider using `getACall()` if there is a need to distingiush between individual calls. */ - Node getReturn() { result = this.getASuccessor(Label::return()) } + cached + Node getReturn() { + Stages::APIStage::ref() and + result = this.getASuccessor(Label::return()) + } /** * Gets a node representing the promised value wrapped in the `Promise` object represented by * this node. */ - Node getPromised() { result = this.getASuccessor(Label::promised()) } + cached + Node getPromised() { + Stages::APIStage::ref() and + result = this.getASuccessor(Label::promised()) + } /** * Gets a node representing the error wrapped in the `Promise` object represented by this node. */ - Node getPromisedError() { result = this.getASuccessor(Label::promisedError()) } + cached + Node getPromisedError() { + Stages::APIStage::ref() and + result = this.getASuccessor(Label::promisedError()) + } /** * Gets a string representation of the lexicographically least among all shortest access paths @@ -211,13 +245,13 @@ module API { * 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) } + Node getASuccessor(Label::ApiLabel 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) } + Node getAPredecessor(Label::ApiLabel lbl) { this = result.getASuccessor(lbl) } /** * Gets a node such that there is an edge in the API graph between this node and the other @@ -283,9 +317,8 @@ module API { length = 0 and result = "" or - exists(Node pred, string lbl, string predpath | + exists(Node pred, Label::ApiLabel 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 @@ -350,6 +383,9 @@ module API { /** Gets a data-flow node that defines this entry point. */ abstract DataFlow::Node getARhs(); + + /** Gets an API-node for this entry point. */ + API::Node getNode() { result = root().getASuccessor(Label::entryPoint(this)) } } /** @@ -433,27 +469,19 @@ module API { 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() - ) - } - /** * 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) { + predicate rhs(TApiNode base, Label::ApiLabel lbl, DataFlow::Node rhs) { hasSemantics(rhs) and ( base = MkRoot() and - rhs = lbl.(EntryPoint).getARhs() + exists(EntryPoint e | + lbl = Label::entryPoint(e) and + rhs = e.getARhs() + ) or exists(string m, string prop | base = MkModuleExport(m) and @@ -565,7 +593,7 @@ module API { */ pragma[noinline] private predicate propertyRead( - DataFlow::SourceNode pred, string propDesc, string lbl, DataFlow::Node ref + DataFlow::SourceNode pred, string propDesc, Label::ApiLabel lbl, DataFlow::Node ref ) { ref = pred.getAPropertyRead() and lbl = Label::memberFromRef(ref) and @@ -589,11 +617,14 @@ module API { * `lbl` in the API graph. */ cached - predicate use(TApiNode base, string lbl, DataFlow::Node ref) { + predicate use(TApiNode base, Label::ApiLabel lbl, DataFlow::Node ref) { hasSemantics(ref) and ( base = MkRoot() and - ref = lbl.(EntryPoint).getAUse() + exists(EntryPoint e | + lbl = Label::entryPoint(e) and + ref = e.getAUse() + ) or // property reads exists(DataFlow::SourceNode src, DataFlow::SourceNode pred, string propDesc | @@ -680,33 +711,6 @@ module API { nd = MkUse(ref) } - /** 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 import semmle.javascript.dataflow.TypeTracking /** @@ -865,10 +869,11 @@ module API { * 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) { + predicate edge(TApiNode pred, Label::ApiLabel lbl, TApiNode succ) { + Stages::APIStage::ref() and exists(string m | pred = MkRoot() and - lbl = Label::mod(m) + lbl = Label::moduleLabel(m) | succ = MkModuleDef(m) or @@ -942,8 +947,6 @@ module API { } } - import Label as EdgeLabel - /** * An `InvokeNode` that is connected to the API graph. * @@ -972,7 +975,8 @@ module API { /** * Gets an API node where a RHS of the node is the `i`th argument to this call. */ - private Node getAParameterCandidate(int i) { result.getARhs() = this.getArgument(i) } + pragma[noinline] + private Node getAParameterCandidate(int i) { result.getARhs() = getArgument(i) } /** Gets the API node for a parameter of this invocation. */ Node getAParameter() { result = this.getParameter(_) } @@ -998,89 +1002,236 @@ module API { /** A `new` call connected to the API graph. */ class NewNode extends InvokeNode, DataFlow::NewNode { } -} -private module Label { - /** Gets the edge label for the module `m`. */ - bindingset[m] - bindingset[result] - string mod(string m) { result = "module " + m } + /** Provides classes modeling the various edges (labels) in the API graph. */ + module Label { + /** A label in the API-graph */ + class ApiLabel extends TLabel { + /** Gets a string representation of this label. */ + string toString() { result = "???" } + } - /** Gets the `member` edge label for member `m`. */ - bindingset[m] - bindingset[result] - string member(string m) { result = "member " + m } + /** Gets the edge label for the module `m`. */ + LabelModule moduleLabel(string m) { result.getMod() = m } - /** Gets the `member` edge label for the unknown member. */ - string unknownMember() { result = "member *" } + /** Gets the `member` edge label for member `m`. */ + bindingset[m] + bindingset[result] + LabelMember member(string m) { result.getProperty() = m } - /** - * Gets a property name referred to by the given dynamic property access, - * allowing one property flow step in the process (to allow flow through imports). - * - * This is to support code patterns where the property name is actually constant, - * but the property name has been factored into a library. - */ - private string getAnIndirectPropName(DataFlow::PropRef ref) { - exists(DataFlow::Node pred | - FlowSteps::propertyFlowStep(pred, ref.getPropertyNameExpr().flow()) and - result = pred.getStringValue() - ) - } + /** Gets the `member` edge label for the unknown member. */ + LabelUnknownMember unknownMember() { any() } - /** - * Gets unique result of `getAnIndirectPropName` if there is one. - */ - private string getIndirectPropName(DataFlow::PropRef ref) { - result = unique(string s | s = getAnIndirectPropName(ref)) - } + /** + * Gets a property name referred to by the given dynamic property access, + * allowing one property flow step in the process (to allow flow through imports). + * + * This is to support code patterns where the property name is actually constant, + * but the property name has been factored into a library. + */ + private string getAnIndirectPropName(DataFlow::PropRef ref) { + exists(DataFlow::Node pred | + FlowSteps::propertyFlowStep(pred, ref.getPropertyNameExpr().flow()) and + result = pred.getStringValue() + ) + } - /** Gets the `member` edge label for the given property reference. */ - string memberFromRef(DataFlow::PropRef pr) { - exists(string pn | pn = pr.getPropertyName() or pn = getIndirectPropName(pr) | - 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$|__)[\\w_$][\\w\\-.$]*") - ) - or - not exists(pr.getPropertyName()) and - not exists(getIndirectPropName(pr)) and - result = unknownMember() - } + /** + * Gets unique result of `getAnIndirectPropName` if there is one. + */ + private string getIndirectPropName(DataFlow::PropRef ref) { + result = unique(string s | s = getAnIndirectPropName(ref)) + } - /** Gets the `instance` edge label. */ - string instance() { result = "instance" } + /** Gets the `member` edge label for the given property reference. */ + ApiLabel memberFromRef(DataFlow::PropRef pr) { + exists(string pn | pn = pr.getPropertyName() or pn = getIndirectPropName(pr) | + 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$|__)[\\w_$][\\w\\-.$]*") + ) + or + not exists(pr.getPropertyName()) and + not exists(getIndirectPropName(pr)) and + result = unknownMember() + } - /** - * 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() >= -1 - } + /** Gets the `instance` edge label. */ + LabelInstance instance() { any() } - /** - * Gets the `parameter` edge label for the `i`th parameter. - * - * The receiver is considered to be parameter -1. - */ - bindingset[i] - string parameter(int i) { result = parameterByStringIndex(i.toString()) } + /** + * Gets the `parameter` edge label for the `i`th parameter. + * + * The receiver is considered to be parameter -1. + */ + LabelParameter parameter(int i) { result.getIndex() = i } + + /** Gets the `parameter` edge label for the receiver. */ + LabelParameter receiver() { result = parameter(-1) } + + /** Gets the `return` edge label. */ + LabelReturn return() { any() } + + /** Gets the `promised` edge label connecting a promise to its contained value. */ + LabelPromised promised() { any() } + + /** Gets the `promisedError` edge label connecting a promise to its rejected value. */ + LabelPromisedError promisedError() { any() } + + /** Gets an entry-point label for the entry-point `e`. */ + LabelEntryPoint entryPoint(API::EntryPoint e) { result.getEntryPoint() = e } + + private import LabelImpl + + private module LabelImpl { + newtype TLabel = + MkLabelModule(string mod) { + exists(Impl::MkModuleExport(mod)) or + exists(Impl::MkModuleImport(mod)) + } or + MkLabelInstance() or + MkLabelMember(string prop) { + exports(_, prop, _) or + exists(any(DataFlow::ClassNode c).getInstanceMethod(prop)) or + prop = "exports" or + prop = any(CanonicalName c).getName() or + prop = any(DataFlow::PropRef p).getPropertyName() or + exists(Impl::MkTypeUse(_, prop)) or + exists(any(Module m).getAnExportedValue(prop)) + } or + MkLabelUnknownMember() or + MkLabelParameter(int i) { + i = + [-1 .. max(int args | + args = any(InvokeExpr invk).getNumArgument() or + args = any(Function f).getNumParameter() + )] or + i = [0 .. 10] + } or + MkLabelReturn() or + MkLabelPromised() or + MkLabelPromisedError() or + MkLabelEntryPoint(API::EntryPoint e) + + /** A label for an entry-point. */ + class LabelEntryPoint extends ApiLabel { + API::EntryPoint e; + + LabelEntryPoint() { this = MkLabelEntryPoint(e) } + + /** Gets the EntryPoint associated with this label. */ + API::EntryPoint getEntryPoint() { result = e } + + override string toString() { result = e } + } + + /** A label that gets a promised value. */ + class LabelPromised extends ApiLabel { + LabelPromised() { this = MkLabelPromised() } + + override string toString() { result = "promised" } + } + + /** A label that gets a rejected promise. */ + class LabelPromisedError extends ApiLabel { + LabelPromisedError() { this = MkLabelPromisedError() } + + override string toString() { result = "promisedError" } + } + + /** A label that gets the return value of a function. */ + class LabelReturn extends ApiLabel { + LabelReturn() { this = MkLabelReturn() } + + override string toString() { result = "return" } + } - /** Gets the `parameter` edge label for the receiver. */ - string receiver() { result = "parameter -1" } + /** A label for a module. */ + class LabelModule extends ApiLabel { + string mod; - /** Gets the `return` edge label. */ - string return() { result = "return" } + LabelModule() { this = MkLabelModule(mod) } - /** Gets the `promised` edge label connecting a promise to its contained value. */ - string promised() { result = "promised" } + /** Gets the module associated with this label. */ + string getMod() { result = mod } + + override string toString() { result = "module " + mod } + } + + /** A label that gets an instance from a `new` call. */ + class LabelInstance extends ApiLabel { + LabelInstance() { this = MkLabelInstance() } + + override string toString() { result = "instance" } + } + + /** A label for the member named `prop`. */ + class LabelMember extends ApiLabel { + string prop; + + LabelMember() { this = MkLabelMember(prop) } + + /** Gets the property associated with this label. */ + string getProperty() { result = prop } + + override string toString() { result = "member " + prop } + } + + /** A label for a member with an unknown name. */ + class LabelUnknownMember extends ApiLabel { + LabelUnknownMember() { this = MkLabelUnknownMember() } + + override string toString() { result = "member *" } + } + + /** A label for parameter `i`. */ + class LabelParameter extends ApiLabel { + int i; + + LabelParameter() { this = MkLabelParameter(i) } + + override string toString() { result = "parameter " + i } + + /** Gets the index of the parameter for this label. */ + int getIndex() { result = i } + } + } + } +} + +/** 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() + ) + ) +} - /** Gets the `promisedError` edge label connecting a promise to its rejected value. */ - string promisedError() { result = "promisedError" } +/** 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() + ) } diff --git a/javascript/ql/lib/semmle/javascript/Constants.qll b/javascript/ql/lib/semmle/javascript/Constants.qll index b9d47fbd8e8a..d914dea8d9c9 100644 --- a/javascript/ql/lib/semmle/javascript/Constants.qll +++ b/javascript/ql/lib/semmle/javascript/Constants.qll @@ -3,10 +3,12 @@ */ import javascript +private import semmle.javascript.internal.CachedStages /** * An expression that evaluates to a constant primitive value. */ +cached abstract class ConstantExpr extends Expr { } /** @@ -16,6 +18,7 @@ module SyntacticConstants { /** * An expression that evaluates to a constant value according to a bottom-up syntactic analysis. */ + cached abstract class SyntacticConstant extends ConstantExpr { } /** @@ -23,8 +26,11 @@ module SyntacticConstants { * * Note that `undefined`, `NaN` and `Infinity` are global variables, and are not covered by this class. */ + cached class PrimitiveLiteralConstant extends SyntacticConstant { + cached PrimitiveLiteralConstant() { + Stages::Ast::ref() and this instanceof NumberLiteral or this instanceof StringLiteral @@ -43,19 +49,27 @@ module SyntacticConstants { /** * A literal null expression. */ - class NullConstant extends SyntacticConstant, NullLiteral { } + cached + class NullConstant extends SyntacticConstant, NullLiteral { + cached + NullConstant() { Stages::Ast::ref() and this = this } + } /** * A unary operation on a syntactic constant. */ + cached class UnaryConstant extends SyntacticConstant, UnaryExpr { + cached UnaryConstant() { getOperand() instanceof SyntacticConstant } } /** * A binary operation on syntactic constants. */ + cached class BinaryConstant extends SyntacticConstant, BinaryExpr { + cached BinaryConstant() { getLeftOperand() instanceof SyntacticConstant and getRightOperand() instanceof SyntacticConstant @@ -65,7 +79,9 @@ module SyntacticConstants { /** * A conditional expression on syntactic constants. */ + cached class ConditionalConstant extends SyntacticConstant, ConditionalExpr { + cached ConditionalConstant() { getCondition() instanceof SyntacticConstant and getConsequent() instanceof SyntacticConstant and @@ -76,7 +92,9 @@ module SyntacticConstants { /** * A use of the global variable `undefined` or `void e`. */ + cached class UndefinedConstant extends SyntacticConstant { + cached UndefinedConstant() { this.(GlobalVarAccess).getName() = "undefined" or this instanceof VoidExpr @@ -86,21 +104,27 @@ module SyntacticConstants { /** * A use of the global variable `NaN`. */ + cached class NaNConstant extends SyntacticConstant { + cached NaNConstant() { this.(GlobalVarAccess).getName() = "NaN" } } /** * A use of the global variable `Infinity`. */ + cached class InfinityConstant extends SyntacticConstant { + cached InfinityConstant() { this.(GlobalVarAccess).getName() = "Infinity" } } /** * An expression that wraps the syntactic constant it evaluates to. */ + cached class WrappedConstant extends SyntacticConstant { + cached WrappedConstant() { getUnderlyingValue() instanceof SyntacticConstant } } @@ -123,6 +147,8 @@ module SyntacticConstants { /** * An expression that evaluates to a constant string. */ +cached class ConstantString extends ConstantExpr { + cached ConstantString() { exists(getStringValue()) } } diff --git a/javascript/ql/lib/semmle/javascript/Expr.qll b/javascript/ql/lib/semmle/javascript/Expr.qll index 6d9a5b6042b3..b99b4c71be82 100644 --- a/javascript/ql/lib/semmle/javascript/Expr.qll +++ b/javascript/ql/lib/semmle/javascript/Expr.qll @@ -89,7 +89,8 @@ class ExprOrType extends @expr_or_type, Documentable { * * Also see `getUnderlyingReference` and `stripParens`. */ - Expr getUnderlyingValue() { result = this } + cached + Expr getUnderlyingValue() { Stages::Ast::ref() and result = this } } /** @@ -274,7 +275,11 @@ private DataFlow::Node getCatchParameterFromStmt(Stmt stmt) { */ class Identifier extends @identifier, ExprOrType { /** Gets the name of this identifier. */ - string getName() { literals(result, _, this) } + cached + string getName() { + Stages::Ast::ref() and + literals(result, _, this) + } override string getAPrimaryQlClass() { result = "Identifier" } } diff --git a/javascript/ql/lib/semmle/javascript/MembershipCandidates.qll b/javascript/ql/lib/semmle/javascript/MembershipCandidates.qll index fe46eff040e7..6c51b487f43c 100644 --- a/javascript/ql/lib/semmle/javascript/MembershipCandidates.qll +++ b/javascript/ql/lib/semmle/javascript/MembershipCandidates.qll @@ -165,6 +165,7 @@ module MembershipCandidate { EnumerationRegExp enumeration; boolean polarity; + pragma[nomagic] RegExpEnumerationCandidate() { exists(DataFlow::MethodCallNode mcn, DataFlow::Node base, string m, DataFlow::Node firstArg | ( diff --git a/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll b/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll index de96295ce219..835c2f7e6269 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll @@ -939,18 +939,21 @@ private predicate basicFlowStepNoBarrier( * This predicate is field insensitive (it does not distinguish between `x` and `x.p`) * and hence should only be used for purposes of approximation. */ -pragma[inline] +pragma[noinline] private predicate exploratoryFlowStep( DataFlow::Node pred, DataFlow::Node succ, DataFlow::Configuration cfg ) { - basicFlowStepNoBarrier(pred, succ, _, cfg) or - exploratoryLoadStep(pred, succ, cfg) or - isAdditionalLoadStoreStep(pred, succ, _, _, cfg) or - // the following three disjuncts taken together over-approximate flow through - // higher-order calls - exploratoryCallbackStep(pred, succ) or - succ = pred.(DataFlow::FunctionNode).getAParameter() or - exploratoryBoundInvokeStep(pred, succ) + isRelevantForward(pred, cfg) and + ( + basicFlowStepNoBarrier(pred, succ, _, cfg) or + exploratoryLoadStep(pred, succ, cfg) or + isAdditionalLoadStoreStep(pred, succ, _, _, cfg) or + // the following three disjuncts taken together over-approximate flow through + // higher-order calls + exploratoryCallbackStep(pred, succ) or + succ = pred.(DataFlow::FunctionNode).getAParameter() or + exploratoryBoundInvokeStep(pred, succ) + ) } /** @@ -1024,6 +1027,7 @@ private string getAPropertyUsedInLoadStore(DataFlow::Configuration cfg) { * Holds if there exists a store-step from `pred` to `succ` under configuration `cfg`, * and somewhere in the program there exists a load-step that could possibly read the stored value. */ +pragma[noinline] private predicate exploratoryForwardStoreStep( DataFlow::Node pred, DataFlow::Node succ, DataFlow::Configuration cfg ) { @@ -1075,8 +1079,10 @@ private string getABackwardsRelevantStoreProperty(DataFlow::Configuration cfg) { private predicate isRelevantForward(DataFlow::Node nd, DataFlow::Configuration cfg) { isSource(nd, cfg, _) and isLive() or - exists(DataFlow::Node mid | isRelevantForward(mid, cfg) | - exploratoryFlowStep(mid, nd, cfg) or + exists(DataFlow::Node mid | + exploratoryFlowStep(mid, nd, cfg) + or + isRelevantForward(mid, cfg) and exploratoryForwardStoreStep(mid, nd, cfg) ) } @@ -1098,11 +1104,10 @@ private predicate isRelevant(DataFlow::Node nd, DataFlow::Configuration cfg) { private predicate isRelevantBackStep( DataFlow::Node mid, DataFlow::Node nd, DataFlow::Configuration cfg ) { + exploratoryFlowStep(nd, mid, cfg) + or isRelevantForward(nd, cfg) and - ( - exploratoryFlowStep(nd, mid, cfg) or - exploratoryBackwardStoreStep(nd, mid, cfg) - ) + exploratoryBackwardStoreStep(nd, mid, cfg) } /** @@ -1273,23 +1278,30 @@ private predicate parameterPropRead( DataFlow::Node arg, string prop, DataFlow::Node succ, DataFlow::Configuration cfg, PathSummary summary ) { - exists(Function f, DataFlow::Node read, DataFlow::Node invk | + exists(Function f, DataFlow::Node read, DataFlow::Node invk, DataFlow::Node parm | + reachesReturn(f, read, cfg, summary) and + parameterPropReadStep(parm, read, prop, cfg, arg, invk, f, succ) + ) +} + +// all the non-recursive parts of parameterPropRead outlined into a precomputed predicate +pragma[noinline] +private predicate parameterPropReadStep( + DataFlow::SourceNode parm, DataFlow::Node read, string prop, DataFlow::Configuration cfg, + DataFlow::Node arg, DataFlow::Node invk, Function f, DataFlow::Node succ +) { + ( not f.isAsyncOrGenerator() and invk = succ or // load from an immediately awaited function call f.isAsync() and invk = getAwaitOperand(succ) - | - exists(DataFlow::SourceNode parm | - callInputStep(f, invk, arg, parm, cfg) and - ( - reachesReturn(f, read, cfg, summary) and - read = parm.getAPropertyRead(prop) - or - reachesReturn(f, read, cfg, summary) and - exists(DataFlow::Node use | parm.flowsTo(use) | isAdditionalLoadStep(use, read, prop, cfg)) - ) - ) + ) and + callInputStep(f, invk, arg, parm, cfg) and + ( + read = parm.getAPropertyRead(prop) + or + exists(DataFlow::Node use | parm.flowsTo(use) | isAdditionalLoadStep(use, read, prop, cfg)) ) } diff --git a/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll b/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll index 3b58b7e046fc..cb3cdec8132f 100644 --- a/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll +++ b/javascript/ql/lib/semmle/javascript/dataflow/TaintTracking.qll @@ -160,10 +160,12 @@ module TaintTracking { * of the standard library. Override `Configuration::isSanitizerGuard` * for analysis-specific taint sanitizer guards. */ + cached abstract class AdditionalSanitizerGuardNode extends SanitizerGuardNode { /** * Holds if this guard applies to the flow in `cfg`. */ + cached abstract predicate appliesTo(Configuration cfg); } @@ -1127,7 +1129,7 @@ module TaintTracking { idx = astNode.getAnOperand() and idx.getPropertyNameExpr() = x and // and the other one is guaranteed to be `undefined` - forex(InferredType tp | tp = undef.getAType() | tp = TTUndefined()) + unique(InferredType tp | tp = pragma[only_bind_into](undef.getAType())) = TTUndefined() ) } diff --git a/javascript/ql/lib/semmle/javascript/frameworks/D3.qll b/javascript/ql/lib/semmle/javascript/frameworks/D3.qll index 49ec68e24241..252e87d3c1dd 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/D3.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/D3.qll @@ -23,7 +23,7 @@ module D3 { or result = API::moduleImport("d3-node").getInstance().getMember("d3") or - result = API::root().getASuccessor(any(D3GlobalEntry i)) + result = any(D3GlobalEntry i).getNode() } /** diff --git a/javascript/ql/lib/semmle/javascript/frameworks/History.qll b/javascript/ql/lib/semmle/javascript/frameworks/History.qll index 05be7ec36ddb..6913fe93daff 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/History.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/History.qll @@ -17,7 +17,7 @@ module History { * Gets a reference to the [`history`](https://npmjs.org/package/history) library. */ private API::Node history() { - result = [API::moduleImport("history"), API::root().getASuccessor(any(HistoryGlobalEntry h))] + result = [API::moduleImport("history"), any(HistoryGlobalEntry h).getNode()] } /** diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Immutable.qll b/javascript/ql/lib/semmle/javascript/frameworks/Immutable.qll index c039199bbd4b..6afb60f73d31 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Immutable.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Immutable.qll @@ -27,7 +27,7 @@ private module Immutable { API::Node immutableImport() { result = API::moduleImport("immutable") or - result = API::root().getASuccessor(any(ImmutableGlobalEntry i)) + result = any(ImmutableGlobalEntry i).getNode() } /** diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Logging.qll b/javascript/ql/lib/semmle/javascript/frameworks/Logging.qll index 4a64ed2a7e17..ea8f97ca9346 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Logging.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Logging.qll @@ -45,7 +45,7 @@ private module Console { */ private API::Node console() { result = API::moduleImport("console") or - result = API::root().getASuccessor(any(ConsoleGlobalEntry e)) + result = any(ConsoleGlobalEntry e).getNode() } /** diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Nest.qll b/javascript/ql/lib/semmle/javascript/frameworks/Nest.qll index d216fac17125..88d626e53985 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Nest.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Nest.qll @@ -151,7 +151,7 @@ module NestJS { private API::Node validationPipe() { result = nestjs().getMember("ValidationPipe") or - result = API::root().getASuccessor(any(ValidationNodeEntry e)) + result = any(ValidationNodeEntry e).getNode() } /** diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Redux.qll b/javascript/ql/lib/semmle/javascript/frameworks/Redux.qll index d26982cef7f6..1451c69ada85 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Redux.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Redux.qll @@ -1111,9 +1111,7 @@ module Redux { /** A heuristic call to `connect`, recognized by it taking arguments named `mapStateToProps` and `mapDispatchToProps`. */ private class HeuristicConnectFunction extends ConnectCall { - HeuristicConnectFunction() { - this = API::root().getASuccessor(any(HeuristicConnectEntryPoint e)).getACall() - } + HeuristicConnectFunction() { this = any(HeuristicConnectEntryPoint e).getNode().getACall() } override API::Node getMapStateToProps() { result = getAParameter() and diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll index 2101f29fdab6..8689140f126d 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -35,7 +35,7 @@ module Vue { API::Node vueLibrary() { result = API::moduleImport("vue") or - result = API::root().getASuccessor(any(GlobalVueEntryPoint e)) + result = any(GlobalVueEntryPoint e).getNode() } /** @@ -51,7 +51,7 @@ module Vue { or result = vueLibrary().getMember("component").getReturn() or - result = API::root().getASuccessor(any(VueFileImportEntryPoint e)) + result = any(VueFileImportEntryPoint e).getNode() } /** diff --git a/javascript/ql/lib/semmle/javascript/internal/CachedStages.qll b/javascript/ql/lib/semmle/javascript/internal/CachedStages.qll index e2df38f0c7c1..cda0727f1360 100644 --- a/javascript/ql/lib/semmle/javascript/internal/CachedStages.qll +++ b/javascript/ql/lib/semmle/javascript/internal/CachedStages.qll @@ -69,6 +69,14 @@ module Stages { exists(any(Expr e).getStringValue()) or any(ASTNode node).isAmbient() + or + exists(any(Identifier e).getName()) + or + exists(any(ExprOrType e).getUnderlyingValue()) + or + exists(ConstantExpr e) + or + exists(SyntacticConstants::NullConstant n) } } @@ -233,6 +241,43 @@ module Stages { } } + /** + * The `APIStage` stage. + */ + cached + module APIStage { + /** + * Always holds. + * Ensures that a predicate is evaluated as part of the APIStage stage. + */ + cached + predicate ref() { 1 = 1 } + + /** + * DONT USE! + * Contains references to each predicate that use the above `ref` predicate. + */ + cached + predicate backref() { + 1 = 1 + or + exists( + API::moduleImport("foo") + .getMember("bar") + .getUnknownMember() + .getAMember() + .getAParameter() + .getPromised() + .getReturn() + .getParameter(2) + .getUnknownMember() + .getInstance() + .getReceiver() + .getPromisedError() + ) + } + } + /** * The `taint` stage. */ @@ -262,6 +307,20 @@ module Stages { exists(Exports::getALibraryInputParameter()) or any(RegExpTerm t).isUsedAsRegExp() + or + any(TaintTracking::AdditionalSanitizerGuardNode e).appliesTo(_) + } + + cached + class DummySanitizer extends TaintTracking::AdditionalSanitizerGuardNode { + cached + DummySanitizer() { none() } + + cached + override predicate appliesTo(TaintTracking::Configuration cfg) { none() } + + cached + override predicate sanitizes(boolean outcome, Expr e) { none() } } } } diff --git a/javascript/ql/lib/semmle/javascript/security/dataflow/ExternalAPIUsedWithUntrustedDataCustomizations.qll b/javascript/ql/lib/semmle/javascript/security/dataflow/ExternalAPIUsedWithUntrustedDataCustomizations.qll index f9eaa7c7ce71..c2a3736e7786 100644 --- a/javascript/ql/lib/semmle/javascript/security/dataflow/ExternalAPIUsedWithUntrustedDataCustomizations.qll +++ b/javascript/ql/lib/semmle/javascript/security/dataflow/ExternalAPIUsedWithUntrustedDataCustomizations.qll @@ -211,11 +211,9 @@ module ExternalAPIUsedWithUntrustedData { node = getNamedParameter(base.getAParameter(), paramName) and result = basename + ".[callback].[param '" + paramName + "']" or - exists(string callbackName, string index | - node = - getNamedParameter(base.getASuccessor("parameter " + index).getMember(callbackName), - paramName) and - index != "-1" and // ignore receiver + exists(string callbackName, int index | + node = getNamedParameter(base.getParameter(index).getMember(callbackName), paramName) and + index != -1 and // ignore receiver result = basename + ".[callback " + index + " '" + callbackName + "'].[param '" + paramName + "']" diff --git a/javascript/ql/lib/semmle/javascript/security/dataflow/RemoteFlowSources.qll b/javascript/ql/lib/semmle/javascript/security/dataflow/RemoteFlowSources.qll index 1049c44caa88..3f7be6ab7544 100644 --- a/javascript/ql/lib/semmle/javascript/security/dataflow/RemoteFlowSources.qll +++ b/javascript/ql/lib/semmle/javascript/security/dataflow/RemoteFlowSources.qll @@ -117,16 +117,20 @@ private class RemoteFlowSourceAccessPath extends JSONString { string getSourceType() { result = sourceType } /** Gets the `i`th component of the access path specifying this remote flow source. */ - string getComponent(int i) { + API::Label::ApiLabel getComponent(int i) { exists(string raw | raw = this.getValue().splitAt(".", i + 1) | i = 0 and - result = "ExternalRemoteFlowSourceSpec " + raw + result = + API::Label::entryPoint(any(ExternalRemoteFlowSourceSpecEntryPoint e | e.getName() = raw)) or i > 0 and - result = API::EdgeLabel::member(raw) + result = API::Label::member(raw) ) } + /** Gets the first part of this access path. E.g. for "window.user.name" the result is "window". */ + string getRootPath() { result = this.getValue().splitAt(".", 1) } + /** Gets the index of the last component of this access path. */ int getMaxComponentIndex() { result = max(int i | exists(this.getComponent(i))) } @@ -154,10 +158,12 @@ private class ExternalRemoteFlowSourceSpecEntryPoint extends API::EntryPoint { string name; ExternalRemoteFlowSourceSpecEntryPoint() { - this = any(RemoteFlowSourceAccessPath s).getComponent(0) and + name = any(RemoteFlowSourceAccessPath s).getRootPath() and this = "ExternalRemoteFlowSourceSpec " + name } + string getName() { result = name } + override DataFlow::SourceNode getAUse() { result = DataFlow::globalVarRef(name) } override DataFlow::Node getARhs() { none() } diff --git a/javascript/ql/src/meta/ApiGraphs/ApiGraphEdges.ql b/javascript/ql/src/meta/ApiGraphs/ApiGraphEdges.ql index 16718a9d1227..23bdad597542 100644 --- a/javascript/ql/src/meta/ApiGraphs/ApiGraphEdges.ql +++ b/javascript/ql/src/meta/ApiGraphs/ApiGraphEdges.ql @@ -12,4 +12,4 @@ import javascript import meta.MetaMetrics select projectRoot(), - count(API::Node pred, string lbl, API::Node succ | succ = pred.getASuccessor(lbl)) + count(API::Node pred, API::Label::ApiLabel lbl, API::Node succ | succ = pred.getASuccessor(lbl)) diff --git a/javascript/ql/test/ApiGraphs/VerifyAssertions.qll b/javascript/ql/test/ApiGraphs/VerifyAssertions.qll index 6cb580e5ce45..532d6ebe3b51 100644 --- a/javascript/ql/test/ApiGraphs/VerifyAssertions.qll +++ b/javascript/ql/test/ApiGraphs/VerifyAssertions.qll @@ -62,7 +62,9 @@ class Assertion extends Comment { i = this.getPathLength() and result = API::root() or - result = this.lookup(i + 1).getASuccessor(this.getEdgeLabel(i)) + result = + this.lookup(i + 1) + .getASuccessor(any(API::Label::ApiLabel label | label.toString() = this.getEdgeLabel(i))) } predicate isNegative() { polarity = "!" } @@ -79,7 +81,11 @@ class Assertion extends Comment { then suffix = "it does have outgoing edges labelled " + - concat(string lbl | exists(nd.getASuccessor(lbl)) | lbl, ", ") + "." + concat(string lbl | + exists(nd.getASuccessor(any(API::Label::ApiLabel label | label.toString() = lbl))) + | + lbl, ", " + ) + "." else suffix = "it has no outgoing edges at all." | result = prefix + " " + suffix