Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
433 changes: 292 additions & 141 deletions javascript/ql/lib/semmle/javascript/ApiGraphs.qll

Large diffs are not rendered by default.

28 changes: 27 additions & 1 deletion javascript/ql/lib/semmle/javascript/Constants.qll
Original file line number Diff line number Diff line change
@@ -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,15 +18,19 @@ module SyntacticConstants {
/**
* An expression that evaluates to a constant value according to a bottom-up syntactic analysis.
*/
cached
abstract class SyntacticConstant extends ConstantExpr { }

/**
* A literal primitive expression.
*
* 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()) }
}
9 changes: 7 additions & 2 deletions javascript/ql/lib/semmle/javascript/Expr.qll
Original file line number Diff line number Diff line change
@@ -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" }
}
Original file line number Diff line number Diff line change
@@ -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 |
(
66 changes: 39 additions & 27 deletions javascript/ql/lib/semmle/javascript/dataflow/Configuration.qll
Original file line number Diff line number Diff line change
@@ -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))
)
}

Original file line number Diff line number Diff line change
@@ -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()
)
}

2 changes: 1 addition & 1 deletion javascript/ql/lib/semmle/javascript/frameworks/D3.qll
Original file line number Diff line number Diff line change
@@ -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()
}

/**
Original file line number Diff line number Diff line change
@@ -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()]
}

/**
Original file line number Diff line number Diff line change
@@ -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()
}

/**
Original file line number Diff line number Diff line change
@@ -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()
}

/**
2 changes: 1 addition & 1 deletion javascript/ql/lib/semmle/javascript/frameworks/Nest.qll
Original file line number Diff line number Diff line change
@@ -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()
}

/**
4 changes: 1 addition & 3 deletions javascript/ql/lib/semmle/javascript/frameworks/Redux.qll
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions javascript/ql/lib/semmle/javascript/frameworks/Vue.qll
Original file line number Diff line number Diff line change
@@ -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()
}

/**
59 changes: 59 additions & 0 deletions javascript/ql/lib/semmle/javascript/internal/CachedStages.qll
Original file line number Diff line number Diff line change
@@ -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() }
}
}
}
Original file line number Diff line number Diff line change
@@ -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 +
"']"
Original file line number Diff line number Diff line change
@@ -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() }
2 changes: 1 addition & 1 deletion javascript/ql/src/meta/ApiGraphs/ApiGraphEdges.ql
Original file line number Diff line number Diff line change
@@ -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))
10 changes: 8 additions & 2 deletions javascript/ql/test/ApiGraphs/VerifyAssertions.qll
Original file line number Diff line number Diff line change
@@ -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