Merge branch 'master' into js/membershiptest

This commit is contained in:
Esben Sparre Andreasen
2020-06-02 08:54:44 +02:00
committed by GitHub
617 changed files with 16819 additions and 10196 deletions

View File

@@ -259,8 +259,7 @@ private class AmdDependencyImport extends Import {
* Gets the module whose absolute path matches this import, if there is only a single such module.
*/
private Module resolveByAbsolutePath() {
count(guessTarget()) = 1 and
result.getFile() = guessTarget()
result.getFile() = unique(File file | file = guessTarget())
}
override Module getImportedModule() {
@@ -291,7 +290,7 @@ private class AmdDependencyImport extends Import {
*/
class AmdModule extends Module {
cached
AmdModule() { strictcount(AmdModuleDefinition def | amdModuleTopLevel(def, this)) = 1 }
AmdModule() { exists(unique(AmdModuleDefinition def | amdModuleTopLevel(def, this))) }
/** Gets the definition of this module. */
AmdModuleDefinition getDefine() { amdModuleTopLevel(result, this) }

View File

@@ -447,9 +447,9 @@ class StmtContainer extends @stmt_container, ASTNode {
*/
module AST {
/**
* A program element that evaluates to a value at runtime. This includes expressions,
* but also function and class declaration statements, as well as TypeScript
* namespace and enum declarations.
* A program element that evaluates to a value or destructures a value at runtime.
* This includes expressions and destructuring patterns, but also function and
* class declaration statements, as well as TypeScript namespace and enum declarations.
*
* Examples:
*

View File

@@ -251,13 +251,15 @@ private module ArrayDataFlow {
/**
* A step for creating an array and storing the elements in the array.
*/
private class ArrayCreationStep extends DataFlow::AdditionalFlowStep, DataFlow::Node {
ArrayCreationStep() { this instanceof DataFlow::ArrayCreationNode }
private class ArrayCreationStep extends DataFlow::AdditionalFlowStep, DataFlow::ArrayCreationNode {
override predicate storeStep(DataFlow::Node element, DataFlow::SourceNode obj, string prop) {
prop = arrayElement() and
element = this.(DataFlow::ArrayCreationNode).getAnElement() and
obj = this
exists(int i |
element = this.getElement(i) and
obj = this and
if this = any(PromiseAllCreation c).getArrayNode()
then prop = arrayElement(i)
else prop = arrayElement()
)
}
}

View File

@@ -299,11 +299,19 @@ class ControlFlowNode extends @cfg_node, Locatable, NodeInStmtContainer {
*/
predicate isStart() { this = any(StmtContainer sc).getStart() }
/**
* Holds if this is a final node of `container`, that is, a CFG node where execution
* of that toplevel or function terminates.
*/
predicate isAFinalNodeOfContainer(StmtContainer container) {
getASuccessor().(SyntheticControlFlowNode).isAFinalNodeOfContainer(container)
}
/**
* Holds if this is a final node, that is, a CFG node where execution of a
* toplevel or function terminates.
*/
predicate isAFinalNode() { getASuccessor().(SyntheticControlFlowNode).isAFinalNode() }
final predicate isAFinalNode() { isAFinalNodeOfContainer(_) }
/**
* Holds if this node is unreachable, that is, it has no predecessors in the CFG.
@@ -361,7 +369,9 @@ class ControlFlowEntryNode extends SyntheticControlFlowNode, @entry_node {
/** A synthetic CFG node marking the exit of a function or toplevel script. */
class ControlFlowExitNode extends SyntheticControlFlowNode, @exit_node {
override predicate isAFinalNode() { any() }
override predicate isAFinalNodeOfContainer(StmtContainer container) {
exit_cfg_node(this, container)
}
override string toString() { result = "exit node of " + getContainer().toString() }
}

View File

@@ -45,8 +45,6 @@ private predicate defn(ControlFlowNode def, Expr lhs, AST::ValueNode rhs) {
exists(EnumMember member | def = member.getIdentifier() |
lhs = def and rhs = member.getInitializer()
)
or
lhs = def and def.(Parameter).getDefault() = rhs
}
/**

View File

@@ -108,6 +108,7 @@ class Expr extends @expr, ExprOrStmt, ExprOrType, AST::ValueNode {
int getIntValue() { none() }
/** Gets the constant string value this expression evaluates to, if any. */
cached
string getStringValue() { none() }
/** Holds if this expression is impure, that is, its evaluation could have side effects. */

View File

@@ -48,6 +48,11 @@ private class DefaultHtmlSanitizerCall extends HtmlSanitizerCall {
or
callee = LodashUnderscore::member("escape")
or
exists(DataFlow::PropRead read | read = callee |
read.getPropertyName() = "sanitize" and
read.getBase().asExpr().(VarAccess).getName() = "DOMPurify"
)
or
exists(string name | name = "encode" or name = "encodeNonUTF" |
callee =
DataFlow::moduleMember("html-entities", _).getAnInstantiation().getAPropertyRead(name) or

View File

@@ -66,10 +66,8 @@ module InclusionTest {
Function callee;
IndirectInclusionTest() {
inner.getEnclosingExpr() = callee.getAReturnedExpr() and
this.getACallee() = callee and
count(this.getACallee()) = 1 and
count(callee.getAReturnedExpr()) = 1 and
inner.getEnclosingExpr() = unique(Expr ret | ret = callee.getAReturnedExpr()) and
callee = unique(Function f | f = this.getACallee()) and
not this.isImprecise() and
inner.getContainedNode().getALocalSource() = DataFlow::parameterNode(callee.getAParameter()) and
inner.getContainerNode().getALocalSource() = DataFlow::parameterNode(callee.getAParameter())

View File

@@ -196,7 +196,7 @@ class JSXName extends Expr {
)
or
exists(JSXQualifiedName qual | qual = this |
result = qual.getNamespace() + ":" + qual.getName()
result = qual.getNamespace().getName() + ":" + qual.getName().getName()
)
}
}

View File

@@ -152,6 +152,18 @@ private class RequireVariable extends Variable {
*/
private predicate moduleInFile(Module m, File f) { m.getFile() = f }
/**
* Holds if `nd` may refer to `require`, either directly or modulo local data flow.
*/
cached
private predicate isRequire(DataFlow::Node nd) {
nd.asExpr() = any(RequireVariable req).getAnAccess() and
// `mjs` files explicitly disallow `require`
not nd.getFile().getExtension() = "mjs"
or
isRequire(nd.getAPredecessor())
}
/**
* A `require` import.
*
@@ -162,12 +174,7 @@ private predicate moduleInFile(Module m, File f) { m.getFile() = f }
* ```
*/
class Require extends CallExpr, Import {
cached
Require() {
any(RequireVariable req).getAnAccess() = getCallee() and
// `mjs` files explicitly disallow `require`
not getFile().getExtension() = "mjs"
}
Require() { isRequire(getCallee().flow()) }
override PathExpr getImportedPath() { result = getArgument(0) }
@@ -257,8 +264,8 @@ private class RequirePath extends PathExprCandidate {
RequirePath() {
this = any(Require req).getArgument(0)
or
exists(RequireVariable req, MethodCallExpr reqres |
reqres.getReceiver() = req.getAnAccess() and
exists(MethodCallExpr reqres |
isRequire(reqres.getReceiver().flow()) and
reqres.getMethodName() = "resolve" and
this = reqres.getArgument(0)
)

View File

@@ -0,0 +1,71 @@
/**
* EXPERIMENTAL. This API may change in the future.
*
* Provides predicates for working with values exported from a package.
*/
import javascript
/**
* Gets the number of occurrences of "/" in `path`.
*/
bindingset[path]
private int countSlashes(string path) { result = count(path.splitAt("/")) - 1 }
/**
* Gets the topmost package.json that appears in the project.
*
* There can be multiple results if the there exists multiple package.json that are equally deeply nested in the folder structure.
* Results are limited to package.json files that are at most nested 2 directories deep.
*/
PackageJSON getTopmostPackageJSON() {
result =
min(PackageJSON j |
countSlashes(j.getFile().getRelativePath()) <= 3
|
j order by countSlashes(j.getFile().getRelativePath())
)
}
/**
* Gets a value exported by the main module from the package.json `packageJSON`.
* The value is either directly the `module.exports` value, a nested property of `module.exports`, or a method on an exported class.
*/
DataFlow::Node getAValueExportedBy(PackageJSON packageJSON) {
result = getAnExportFromModule(packageJSON.getMainModule())
or
result = getAValueExportedBy(packageJSON).(DataFlow::PropWrite).getRhs()
or
exists(DataFlow::SourceNode callee |
callee = getAValueExportedBy(packageJSON).(DataFlow::NewNode).getCalleeNode().getALocalSource()
|
result = callee.getAPropertyRead("prototype").getAPropertyWrite()
or
result = callee.(DataFlow::ClassNode).getAnInstanceMethod()
)
or
result = getAValueExportedBy(packageJSON).getALocalSource()
or
result = getAValueExportedBy(packageJSON).(DataFlow::SourceNode).getAPropertyReference()
or
exists(Module mod |
mod = getAValueExportedBy(packageJSON).getEnclosingExpr().(Import).getImportedModule()
|
result = getAnExportFromModule(mod)
)
or
exists(DataFlow::ClassNode cla | cla = getAValueExportedBy(packageJSON) |
result = cla.getAnInstanceMethod() or
result = cla.getAStaticMethod() or
result = cla.getConstructor()
)
}
/**
* Gets an exported node from the module `mod`.
*/
private DataFlow::Node getAnExportFromModule(Module mod) {
result.analyze().getAValue() = mod.(NodeModule).getAModuleExportsValue()
or
exists(ASTNode export | result.getEnclosingExpr() = export | mod.exports(_, export))
}

View File

@@ -85,7 +85,7 @@ private class ES2015PromiseDefinition extends PromiseDefinition, DataFlow::NewNo
*/
abstract class PromiseCreationCall extends DataFlow::CallNode {
/**
* Gets the value this promise is resolved with.
* Gets a value this promise is resolved with.
*/
abstract DataFlow::Node getValue();
}
@@ -95,6 +95,16 @@ abstract class PromiseCreationCall extends DataFlow::CallNode {
*/
abstract class ResolvedPromiseDefinition extends PromiseCreationCall { }
/**
* A promise that is created using a `Promise.all(array)` call.
*/
abstract class PromiseAllCreation extends PromiseCreationCall {
/**
* Gets a node for the array of values given to the `Promise.all(array)` call.
*/
abstract DataFlow::Node getArrayNode();
}
/**
* A resolved promise created by the standard ECMAScript 2015 `Promise.resolve` function.
*/
@@ -121,6 +131,15 @@ class AggregateES2015PromiseDefinition extends PromiseCreationCall {
}
}
/**
* An aggregated promise created using `Promise.all()`.
*/
class ES2015PromiseAllDefinition extends AggregateES2015PromiseDefinition, PromiseAllCreation {
ES2015PromiseAllDefinition() { this.getCalleeName() = "all" }
override DataFlow::Node getArrayNode() { result = getArgument(0) }
}
/**
* Common predicates shared between type-tracking and data-flow for promises.
*/
@@ -303,16 +322,27 @@ private module PromiseFlow {
CreationStep() { this = promise }
override predicate store(DataFlow::Node pred, DataFlow::SourceNode succ, string prop) {
not promise instanceof PromiseAllCreation and
prop = valueProp() and
pred = promise.getValue() and
succ = this
or
prop = valueProp() and
pred = promise.(PromiseAllCreation).getArrayNode() and
succ = this
}
override predicate loadStore(DataFlow::Node pred, DataFlow::Node succ, string prop) {
// Copy the value of a resolved promise to the value of this promise.
not promise instanceof PromiseAllCreation and
prop = valueProp() and
pred = promise.getValue() and
succ = this
or
promise instanceof PromiseAllCreation and
prop = valueProp() and
pred = promise.(PromiseAllCreation).getArrayNode() and
succ = this
}
}
@@ -446,7 +476,11 @@ predicate promiseTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
pred = succ.(PromiseDefinition).getResolveParameter().getACall().getArgument(0)
or
// from `x` to `Promise.resolve(x)`
pred = succ.(PromiseCreationCall).getValue()
pred = succ.(PromiseCreationCall).getValue() and
not succ instanceof PromiseAllCreation
or
// from `arr` to `Promise.all(arr)`
pred = succ.(PromiseAllCreation).getArrayNode()
or
exists(DataFlow::MethodCallNode thn | thn.getMethodName() = "then" |
// from `p` to `x` in `p.then(x => ...)`
@@ -533,6 +567,15 @@ module Bluebird {
result = getArgument(0).getALocalSource().(DataFlow::ArrayCreationNode).getAnElement()
}
}
/**
* A promise created using `Promise.all`:
*/
class BluebirdPromiseAllDefinition extends AggregateBluebirdPromiseDefinition, PromiseAllCreation {
BluebirdPromiseAllDefinition() { this.getCalleeName() = "all" }
override DataFlow::Node getArrayNode() { result = getArgument(0) }
}
}
/**

View File

@@ -64,10 +64,8 @@ module StringOps {
Function callee;
IndirectStartsWith() {
inner.getEnclosingExpr() = callee.getAReturnedExpr() and
this.getACallee() = callee and
count(this.getACallee()) = 1 and
count(callee.getAReturnedExpr()) = 1 and
inner.getEnclosingExpr() = unique(Expr ret | ret = callee.getAReturnedExpr()) and
callee = unique(Function f | f = this.getACallee()) and
not this.isImprecise() and
inner.getBaseString().getALocalSource().getEnclosingExpr() = callee.getAParameter() and
inner.getSubstring().getALocalSource().getEnclosingExpr() = callee.getAParameter()
@@ -295,10 +293,8 @@ module StringOps {
Function callee;
IndirectEndsWith() {
inner.getEnclosingExpr() = callee.getAReturnedExpr() and
this.getACallee() = callee and
count(this.getACallee()) = 1 and
count(callee.getAReturnedExpr()) = 1 and
inner.getEnclosingExpr() = unique(Expr ret | ret = callee.getAReturnedExpr()) and
callee = unique(Function f | f = this.getACallee()) and
not this.isImprecise() and
inner.getBaseString().getALocalSource().getEnclosingExpr() = callee.getAParameter() and
inner.getSubstring().getALocalSource().getEnclosingExpr() = callee.getAParameter()

View File

@@ -116,7 +116,7 @@ class XMLFile extends XMLParent, File {
XMLFile() { xmlEncoding(this, _) }
/** Gets a printable representation of this XML file. */
override string toString() { result = XMLParent.super.toString() }
override string toString() { result = getName() }
/** Gets the name of this XML file. */
override string getName() { result = File.super.getAbsolutePath() }
@@ -236,7 +236,7 @@ class XMLElement extends @xmlelement, XMLParent, XMLLocatable {
string getAttributeValue(string name) { result = this.getAttribute(name).getValue() }
/** Gets a printable representation of this XML element. */
override string toString() { result = XMLParent.super.toString() }
override string toString() { result = getName() }
}
/**

View File

@@ -438,7 +438,10 @@ private predicate barrierGuardBlocksNode(BarrierGuardNode guard, DataFlow::Node
barrierGuardIsRelevant(guard) and
exists(AccessPath p, BasicBlock bb, ConditionGuardNode cond, boolean outcome |
nd = DataFlow::valueNode(p.getAnInstanceIn(bb)) and
guard.getEnclosingExpr() = cond.getTest() and
(
guard.getEnclosingExpr() = cond.getTest() or
guard = cond.getTest().flow().getImmediatePredecessor+()
) and
outcome = cond.getOutcome() and
barrierGuardBlocksAccessPath(guard, outcome, p, label) and
cond.dominates(bb)
@@ -607,6 +610,16 @@ module PseudoProperties {
*/
string arrayElement() { result = pseudoProperty("arrayElement") }
/**
* Gets a pseudo-property for the location of the `i`th element in an `Array`.
*/
bindingset[i]
string arrayElement(int i) {
i < 5 and result = i.toString()
or
result = arrayElement()
}
/**
* Gets a pseudo-property for the location of elements in some array-like object. (Set, Array, or Iterator).
*/
@@ -1601,6 +1614,9 @@ class MidPathNode extends PathNode, MkMidNode {
nd.(DataFlow::SsaDefinitionNode).getSsaVariable().getDefinition() instanceof
SsaImplicitDefinition
or
// Skip SSA definition of parameter as its location coincides with the parameter node
nd = DataFlow::ssaDefinitionNode(SSA::definition(any(SimpleParameter p)))
or
// Skip to the top of big left-leaning string concatenation trees.
nd = any(AddExpr add).flow() and
nd = any(AddExpr add).getAnOperand().flow()

View File

@@ -21,32 +21,10 @@
import javascript
private import internal.CallGraphs
private import internal.FlowSteps as FlowSteps
private import internal.DataFlowNode
private import internal.AnalyzedParameters
module DataFlow {
cached
private newtype TNode =
TValueNode(AST::ValueNode nd) or
TSsaDefNode(SsaDefinition d) or
TCapturedVariableNode(LocalVariable v) { v.isCaptured() } or
TPropNode(@property p) or
TRestPatternNode(DestructuringPattern dp, Expr rest) { rest = dp.getRest() } or
TDestructuringPatternNode(DestructuringPattern dp) or
TElementPatternNode(ArrayPattern ap, Expr p) { p = ap.getElement(_) } or
TElementNode(ArrayExpr arr, Expr e) { e = arr.getAnElement() } or
TReflectiveCallNode(MethodCallExpr ce, string kind) {
ce.getMethodName() = kind and
(kind = "call" or kind = "apply")
} or
TThisNode(StmtContainer f) { f.(Function).getThisBinder() = f or f instanceof TopLevel } or
TUnusedParameterNode(SimpleParameter p) { not exists(SSA::definition(p)) } or
TDestructuredModuleImportNode(ImportDeclaration decl) {
exists(decl.getASpecifier().getImportedName())
} or
THtmlAttributeNode(HTML::Attribute attr) or
TExceptionalFunctionReturnNode(Function f) or
TExceptionalInvocationReturnNode(InvokeExpr e) or
TGlobalAccessPathRoot()
/**
* A node in the data flow graph.
*/
@@ -90,13 +68,11 @@ module DataFlow {
/**
* Gets the expression enclosing this data flow node.
* In most cases the result is the same as `asExpr()`, however this method
* additionally the `InvokeExpr` corresponding to reflective calls, and the `Parameter`
* for a `DataFlow::ParameterNode`.
* additionally includes the `InvokeExpr` corresponding to reflective calls.
*/
Expr getEnclosingExpr() {
result = asExpr() or
this = DataFlow::reflectiveCallNode(result) or
result = this.(ParameterNode).getParameter()
this = DataFlow::reflectiveCallNode(result)
}
/** Gets the AST node corresponding to this data flow node, if any. */
@@ -218,8 +194,7 @@ module DataFlow {
// IIFE call -> return value of IIFE
exists(Function fun |
localCall(this.asExpr(), fun) and
result = fun.getAReturnedExpr().flow() and
strictcount(fun.getAReturnedExpr()) = 1 and
result = unique(Expr ret | ret = fun.getAReturnedExpr()).flow() and
not fun.getExit().isJoin() // can only reach exit by the return statement
)
}
@@ -252,7 +227,7 @@ module DataFlow {
*/
private JSDocTypeExpr getFallbackTypeAnnotation() {
exists(BindingPattern pattern |
this = lvalueNode(pattern) and
this = valueNode(pattern) and
not ast_node_type(pattern, _) and
result = pattern.getTypeAnnotation()
)
@@ -282,8 +257,8 @@ module DataFlow {
}
/**
* An expression or a declaration of a function, class, namespace or enum,
* viewed as a node in the data flow graph.
* A node in the data flow graph which corresponds to an expression,
* destructuring pattern, or declaration of a function, class, namespace, or enum.
*
* Examples:
* ```js
@@ -391,30 +366,6 @@ module DataFlow {
override ASTNode getAstNode() { result = rest }
}
/**
* A node in the data flow graph which corresponds to the value destructured by an
* object or array pattern.
*/
private class DestructuringPatternNode extends Node, TDestructuringPatternNode {
DestructuringPattern pattern;
DestructuringPatternNode() { this = TDestructuringPatternNode(pattern) }
override BasicBlock getBasicBlock() { result = pattern.getBasicBlock() }
override predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
pattern.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
override string toString() { result = pattern.toString() }
override File getFile() { result = pattern.getFile() }
override ASTNode getAstNode() { result = pattern }
}
/**
* A node in the data flow graph which corresponds to an element pattern of an
* array pattern.
@@ -760,10 +711,6 @@ module DataFlow {
parameterNode(paramNode, param)
|
result = paramNode
or
// special case: there is no SSA flow step for unused parameters
paramNode instanceof UnusedParameterNode and
result = param.getDefault().flow()
)
}
@@ -851,7 +798,7 @@ module DataFlow {
/** Gets the value pattern of this property pattern. */
Expr getValuePattern() { result = prop.getValuePattern() }
override Node getBase() { result = TDestructuringPatternNode(prop.getObjectPattern()) }
override Node getBase() { result = TValueNode(prop.getObjectPattern()) }
override Expr getPropertyNameExpr() { result = prop.getNameExpr() }
@@ -864,7 +811,7 @@ module DataFlow {
* for `[ ...elts ] = arr`.
*/
private class RestPatternAsPropRead extends PropRead, RestPatternNode {
override Node getBase() { result = TDestructuringPatternNode(pattern) }
override Node getBase() { result = TValueNode(pattern) }
override Expr getPropertyNameExpr() { none() }
@@ -877,7 +824,7 @@ module DataFlow {
* for `y`.
*/
private class ElementPatternAsPropRead extends PropRead, ElementPatternNode {
override Node getBase() { result = TDestructuringPatternNode(pattern) }
override Node getBase() { result = TValueNode(pattern) }
override Expr getPropertyNameExpr() { none() }
@@ -924,32 +871,6 @@ module DataFlow {
override string getPropertyName() { none() }
}
/**
* A data flow node representing an unused parameter.
*
* This case exists to ensure all parameters have a corresponding data-flow node.
* In most cases, parameters are represented by SSA definitions or destructuring pattern nodes.
*/
private class UnusedParameterNode extends DataFlow::Node, TUnusedParameterNode {
SimpleParameter p;
UnusedParameterNode() { this = TUnusedParameterNode(p) }
override string toString() { result = p.toString() }
override ASTNode getAstNode() { result = p }
override BasicBlock getBasicBlock() { result = p.getBasicBlock() }
override predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
p.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
override File getFile() { result = p.getFile() }
}
/**
* A data flow node representing an HTML attribute.
*/
@@ -1303,7 +1224,7 @@ module DataFlow {
/**
* INTERNAL: Use `parameterNode(Parameter)` instead.
*/
predicate parameterNode(DataFlow::Node nd, Parameter p) { nd = lvalueNode(p) }
predicate parameterNode(DataFlow::Node nd, Parameter p) { nd = valueNode(p) }
/**
* INTERNAL: Use `thisNode(StmtContainer container)` instead.
@@ -1355,9 +1276,7 @@ module DataFlow {
result = TSsaDefNode(ssa)
)
or
result = TDestructuringPatternNode(lvalue)
or
result = TUnusedParameterNode(lvalue)
result = TValueNode(lvalue.(DestructuringPattern))
}
/**
@@ -1412,6 +1331,17 @@ module DataFlow {
succ = lvalueNode(def.getTarget())
)
or
exists(SimpleParameter param |
pred = valueNode(param) and // The value node represents the incoming argument
succ = lvalueNode(param) // The SSA node represents the parameters's local variable
)
or
exists(Expr arg, Parameter param |
localArgumentPassing(arg, param) and
pred = valueNode(arg) and
succ = valueNode(param)
)
or
exists(PropertyPattern pattern |
pred = TPropNode(pattern) and
succ = lvalueNode(pattern.getValuePattern())
@@ -1547,8 +1477,7 @@ module DataFlow {
*/
private AST::ValueNode defSourceNode(VarDef def) {
result = def.getSource() or
result = def.getDestructuringSource() or
localArgumentPassing(result, def)
result = def.getDestructuringSource()
}
/**
@@ -1594,8 +1523,15 @@ module DataFlow {
e instanceof FunctionBindExpr
or
e instanceof TaggedTemplateExpr
or
e instanceof Parameter and
not localArgumentPassing(_, e) and
not isAnalyzedParameter(e) and
not e.(Parameter).isRestParameter()
)
or
nd.(AnalyzedNode).hasAdditionalIncompleteness(cause)
or
nd.asExpr() instanceof ExternalModuleReference and
cause = "import"
or
@@ -1623,18 +1559,12 @@ module DataFlow {
exists(PropertyPattern p | nd = TPropNode(p)) and cause = "heap"
or
nd instanceof TElementPatternNode and cause = "heap"
or
nd instanceof UnusedParameterNode and cause = "call"
}
/**
* Holds if definition `def` cannot be completely analyzed due to `cause`.
*/
private predicate defIsIncomplete(VarDef def, Incompleteness cause) {
def instanceof Parameter and
not localArgumentPassing(_, def) and
cause = "call"
or
def instanceof ImportSpecifier and
cause = "import"
or

View File

@@ -12,6 +12,8 @@ private predicate isEscape(DataFlow::Node escape, string cause) {
or
escape = any(DataFlow::FunctionNode fun).getAReturn() and cause = "return"
or
escape = any(YieldExpr yield).getOperand().flow() and cause = "yield"
or
escape = any(ThrowStmt t).getExpr().flow() and cause = "throw"
or
escape = any(GlobalVariable v).getAnAssignedExpr().flow() and cause = "global"

View File

@@ -241,9 +241,6 @@ module TaintTracking {
*/
private predicate heapStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Expr e, Expr f | e = succ.asExpr() and f = pred.asExpr() |
// arrays with tainted elements and objects with tainted property names are tainted
e.(ArrayExpr).getAnElement() = f
or
exists(Property prop | e.(ObjectExpr).getAProperty() = prop |
prop.isComputed() and f = prop.getNameExpr()
)
@@ -258,6 +255,10 @@ module TaintTracking {
e.(ArrayExpr).getAnElement().(SpreadElement).getOperand() = f
)
or
// arrays with tainted elements and objects with tainted property names are tainted
succ.(DataFlow::ArrayCreationNode).getAnElement() = pred and
not any(PromiseAllCreation call).getArrayNode() = succ
or
// reading from a tainted object yields a tainted result
succ.(DataFlow::PropRead).getBase() = pred
or
@@ -778,7 +779,8 @@ module TaintTracking {
*/
class AdHocWhitelistCheckSanitizer extends SanitizerGuardNode, DataFlow::CallNode {
AdHocWhitelistCheckSanitizer() {
getCalleeName().regexpMatch("(?i).*((?<!un)safe|whitelist|allow|(?<!un)auth(?!or\\b)).*") and
getCalleeName()
.regexpMatch("(?i).*((?<!un)safe|whitelist|(?<!in)valid|allow|(?<!un)auth(?!or\\b)).*") and
getNumArgument() = 1
}
@@ -828,6 +830,28 @@ module TaintTracking {
/** DEPRECATED. This class has been renamed to `MembershipTestSanitizer`. */
deprecated class StringInclusionSanitizer = MembershipTestSanitizer;
/**
* A test of form `x.length === "0"`, preventing `x` from being tainted.
*/
class IsEmptyGuard extends AdditionalSanitizerGuardNode, DataFlow::ValueNode {
override EqualityTest astNode;
boolean polarity;
Expr operand;
IsEmptyGuard() {
astNode.getPolarity() = polarity and
astNode.getAnOperand().(ConstantExpr).getIntValue() = 0 and
exists(DataFlow::PropRead read | read.asExpr() = astNode.getAnOperand() |
read.getBase().asExpr() = operand and
read.getPropertyName() = "length"
)
}
override predicate sanitizes(boolean outcome, Expr e) { polarity = outcome and e = operand }
override predicate appliesTo(Configuration cfg) { any() }
}
/** DEPRECATED. This class has been renamed to `MembershipTestSanitizer`. */
deprecated class InclusionSanitizer = MembershipTestSanitizer;

View File

@@ -100,7 +100,7 @@ class AnalyzedNode extends DataFlow::Node {
boolean getTheBooleanValue() { forex(boolean bv | bv = getABooleanValue() | result = bv) }
/** Gets the unique type inferred for this node, if any. */
InferredType getTheType() { count(getAType()) = 1 and result = getAType() }
InferredType getTheType() { result = unique(InferredType t | t = getAType()) }
/**
* Gets a pretty-printed representation of all types inferred for this node
@@ -156,6 +156,14 @@ class AnalyzedNode extends DataFlow::Node {
/** Holds if the flow analysis can infer at least one abstract value for this node. */
predicate hasFlow() { exists(getAValue()) }
/**
* INTERNAL. Use `isIncomplete()` instead.
*
* Subclasses may override this to contribute additional incompleteness to this node
* without overriding `isIncomplete()`.
*/
predicate hasAdditionalIncompleteness(DataFlow::Incompleteness cause) { none() }
}
/**
@@ -255,14 +263,14 @@ class AnalyzedFunction extends DataFlow::AnalyzedValueNode {
* of functions that cannot actually complete normally, since it does not
* account for `finally` blocks and does not check reachability.
*/
private predicate mayReturnImplicitly() {
exists(ConcreteControlFlowNode final |
final.getContainer() = astNode and
final.isAFinalNode() and
not final instanceof ReturnStmt and
not final instanceof ThrowStmt
)
}
private predicate mayReturnImplicitly() { terminalNode(astNode, any(ExprOrStmt st)) }
}
pragma[noinline]
private predicate terminalNode(Function f, ControlFlowNode final) {
final.isAFinalNodeOfContainer(f) and
not final instanceof ReturnStmt and
not final instanceof ThrowStmt
}
/**

View File

@@ -0,0 +1,44 @@
private import javascript
private import VariableTypeInference
/**
* Holds if `p` is analyzed precisely by the type inference.
*/
pragma[nomagic]
predicate isAnalyzedParameter(Parameter p) {
exists(FunctionWithAnalyzedParameters f, int parmIdx | p = f.getParameter(parmIdx) |
// we cannot track flow into rest parameters
not p.(Parameter).isRestParameter()
)
}
/**
* A parameter whose value is propagated interprocedurally.
*/
class AnalyzedParameter extends AnalyzedValueNode {
override Parameter astNode;
AnalyzedParameter() { isAnalyzedParameter(astNode) }
FunctionWithAnalyzedParameters getFunction() { astNode = result.getAParameter() }
override AbstractValue getALocalValue() {
exists(DataFlow::AnalyzedNode pred |
getFunction().argumentPassing(astNode, pred.asExpr()) and
result = pred.getALocalValue()
)
or
not getFunction().mayReceiveArgument(astNode) and
result = TAbstractUndefined()
or
result = astNode.getDefault().analyze().getALocalValue()
}
override predicate hasAdditionalIncompleteness(DataFlow::Incompleteness cause) {
getFunction().isIncomplete(cause)
or
not getFunction().argumentPassing(astNode, _) and
getFunction().mayReceiveArgument(astNode) and
cause = "call"
}
}

View File

@@ -239,39 +239,41 @@ private class AnalyzedBinaryExpr extends DataFlow::AnalyzedValueNode {
}
/**
* Gets a primitive type to which the local value of `e` can be coerced.
* Gets the `n`th operand of the given `+` or `+=` expression.
*/
private PrimitiveType getALocalPrimitiveType(Expr e) {
result = e.analyze().getALocalValue().toPrimitive().getType()
pragma[nomagic]
private DataFlow::AnalyzedValueNode getAddOperand(Expr e, int n) {
(e instanceof AddExpr or e instanceof AssignAddExpr) and
result = DataFlow::valueNode(e.getChildExpr(n))
}
/**
* Holds if `e` may hold a string value.
* Gets a primitive type of the `n`th operand of the given `+` or `+=` expression.
*/
private predicate maybeString(Expr e) { getALocalPrimitiveType(e) = TTString() }
/**
* Holds if `e` may hold a non-string value.
*/
private predicate maybeNonString(Expr e) { getALocalPrimitiveType(e) != TTString() }
pragma[noopt]
private PrimitiveType getAnAddOperandPrimitiveType(Expr e, int n) {
exists(DataFlow::AnalyzedValueNode operand, AbstractValue value, AbstractValue prim |
operand = getAddOperand(e, n) and
value = operand.getALocalValue() and
prim = value.toPrimitive() and
result = prim.getType() and
result instanceof PrimitiveType
)
}
/**
* Holds if `e` is a `+` or `+=` expression that could be interpreted as a string append
* (as opposed to a numeric addition) at runtime.
*/
private predicate isStringAppend(Expr e) {
(e instanceof AddExpr or e instanceof AssignAddExpr) and
maybeString(e.getAChildExpr())
}
private predicate isStringAppend(Expr e) { getAnAddOperandPrimitiveType(e, _) = TTString() }
/**
* Holds if `e` is a `+` or `+=` expression that could be interpreted as a numeric addition
* (as opposed to a string append) at runtime.
*/
private predicate isAddition(Expr e) {
(e instanceof AddExpr or e instanceof AssignAddExpr) and
maybeNonString(e.getChildExpr(0)) and
maybeNonString(e.getChildExpr(1))
getAnAddOperandPrimitiveType(e, 0) != TTString() and
getAnAddOperandPrimitiveType(e, 1) != TTString()
}
/**

View File

@@ -0,0 +1,32 @@
/**
* INTERNAL: Do not use outside the data flow library.
*
* Contains the raw data type underlying `DataFlow::Node`.
*/
private import javascript
/**
* The raw data type underlying `DataFlow::Node`.
*/
cached
newtype TNode =
TValueNode(AST::ValueNode nd) or
TSsaDefNode(SsaDefinition d) or
TCapturedVariableNode(LocalVariable v) { v.isCaptured() } or
TPropNode(@property p) or
TRestPatternNode(DestructuringPattern dp, Expr rest) { rest = dp.getRest() } or
TElementPatternNode(ArrayPattern ap, Expr p) { p = ap.getElement(_) } or
TElementNode(ArrayExpr arr, Expr e) { e = arr.getAnElement() } or
TReflectiveCallNode(MethodCallExpr ce, string kind) {
ce.getMethodName() = kind and
(kind = "call" or kind = "apply")
} or
TThisNode(StmtContainer f) { f.(Function).getThisBinder() = f or f instanceof TopLevel } or
TDestructuredModuleImportNode(ImportDeclaration decl) {
exists(decl.getASpecifier().getImportedName())
} or
THtmlAttributeNode(HTML::Attribute attr) or
TExceptionalFunctionReturnNode(Function f) or
TExceptionalInvocationReturnNode(InvokeExpr e) or
TGlobalAccessPathRoot()

View File

@@ -92,6 +92,13 @@ private module CachedSteps {
cached
predicate calls(DataFlow::InvokeNode invk, Function f) { f = invk.getACallee(0) }
private predicate callsBoundInternal(
DataFlow::InvokeNode invk, Function f, int boundArgs, boolean contextDependent
) {
CallGraph::getABoundFunctionReference(f.flow(), boundArgs, contextDependent)
.flowsTo(invk.getCalleeNode())
}
/**
* Holds if `invk` may invoke a bound version of `f` with `boundArgs` already bound.
*
@@ -101,7 +108,7 @@ private module CachedSteps {
*/
cached
predicate callsBound(DataFlow::InvokeNode invk, Function f, int boundArgs) {
CallGraph::getABoundFunctionReference(f.flow(), boundArgs, false).flowsTo(invk.getCalleeNode())
callsBoundInternal(invk, f, boundArgs, false)
}
/**
@@ -111,10 +118,10 @@ private module CachedSteps {
*/
cached
predicate exploratoryBoundInvokeStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::InvokeNode invk, DataFlow::FunctionNode f, int i, int boundArgs |
CallGraph::getABoundFunctionReference(f, boundArgs, _).flowsTo(invk.getCalleeNode()) and
exists(DataFlow::InvokeNode invk, Function f, int i, int boundArgs |
callsBoundInternal(invk, f, boundArgs, _) and
pred = invk.getArgument(i) and
succ = f.getParameter(i + boundArgs)
succ = DataFlow::parameterNode(f.getParameter(i + boundArgs))
)
}
@@ -144,11 +151,14 @@ private module CachedSteps {
) {
calls(invk, f) and
(
exists(int i, Parameter p |
f.getParameter(i) = p and
not p.isRestParameter() and
arg = invk.getArgument(i) and
parm = DataFlow::parameterNode(p)
exists(int i | arg = invk.getArgument(i) |
exists(Parameter p |
f.getParameter(i) = p and
not p.isRestParameter() and
parm = DataFlow::parameterNode(p)
)
or
parm = reflectiveParameterAccess(f, i)
)
or
arg = invk.(DataFlow::CallNode).getReceiver() and
@@ -178,6 +188,22 @@ private module CachedSteps {
)
}
/**
* Gets a data-flow node inside `f` that refers to the `arguments` object of `f`.
*/
private DataFlow::Node argumentsAccess(Function f) {
result.getContainer().getEnclosingContainer*() = f and
result.analyze().getAValue().(AbstractArguments).getFunction() = f
}
/**
* Gets a data-flow node that refers to the `i`th parameter of `f` through its `arguments`
* object.
*/
private DataFlow::SourceNode reflectiveParameterAccess(Function f, int i) {
result.(DataFlow::PropRead).accesses(argumentsAccess(f), any(string p | i = p.toInt()))
}
/**
* Holds if there is a flow step from `pred` to `succ` through parameter passing
* to a function call.

View File

@@ -187,8 +187,7 @@ private class IIFEWithAnalyzedReturnFlow extends CallWithAnalyzedReturnFlow {
*/
private VarAccess getOnlyAccess(FunctionDeclStmt fn, LocalVariable v) {
v = fn.getVariable() and
result = v.getAnAccess() and
strictcount(v.getAnAccess()) = 1
result = unique(VarAccess acc | acc = v.getAnAccess())
}
/** A function that only is used locally, making it amenable to type inference. */

View File

@@ -6,6 +6,7 @@
private import javascript
private import AbstractValuesImpl
private import AnalyzedParameters
private import semmle.javascript.dataflow.InferredTypes
private import semmle.javascript.dataflow.Refinements
@@ -120,7 +121,7 @@ class AnalyzedVarDef extends VarDef {
* due to the given `cause`.
*/
predicate isIncomplete(DataFlow::Incompleteness cause) {
this instanceof Parameter and cause = "call"
this instanceof Parameter and DataFlow::valueNode(this).(AnalyzedValueNode).isIncomplete(cause)
or
this instanceof ImportSpecifier and cause = "import"
or
@@ -143,47 +144,21 @@ class AnalyzedVarDef extends VarDef {
/**
* Flow analysis for simple parameters of selected functions.
*/
private class AnalyzedParameter extends AnalyzedVarDef, @vardecl {
AnalyzedParameter() {
exists(FunctionWithAnalyzedParameters f, int parmIdx | this = f.getParameter(parmIdx) |
// we cannot track flow into rest parameters
not this.(Parameter).isRestParameter()
)
}
/** Gets the function this is a parameter of. */
FunctionWithAnalyzedParameters getFunction() { this = result.getAParameter() }
override DataFlow::AnalyzedNode getRhs() {
getFunction().argumentPassing(this, result.asExpr()) or
result = AnalyzedVarDef.super.getRhs()
}
private class AnalyzedParameterAsVarDef extends AnalyzedVarDef, @vardecl {
AnalyzedParameterAsVarDef() { this instanceof Parameter }
override AbstractValue getAnRhsValue() {
result = AnalyzedVarDef.super.getAnRhsValue()
or
not getFunction().mayReceiveArgument(this) and
result = TAbstractUndefined()
}
override predicate isIncomplete(DataFlow::Incompleteness cause) {
getFunction().isIncomplete(cause)
or
not getFunction().argumentPassing(this, _) and
getFunction().mayReceiveArgument(this) and
cause = "call"
result = DataFlow::valueNode(this).(AnalyzedValueNode).getALocalValue()
}
}
/**
* Flow analysis for simple rest parameters.
*/
private class AnalyzedRestParameter extends AnalyzedVarDef, @vardecl {
AnalyzedRestParameter() { this.(Parameter).isRestParameter() }
private class AnalyzedRestParameter extends AnalyzedValueNode {
AnalyzedRestParameter() { astNode.(Parameter).isRestParameter() }
override AbstractValue getAnRhsValue() { result = TAbstractOtherObject() }
override predicate isIncomplete(DataFlow::Incompleteness cause) { none() }
override AbstractValue getALocalValue() { result = TAbstractOtherObject() }
}
/**
@@ -412,9 +387,7 @@ private class AnalyzedGlobalVarUse extends DataFlow::AnalyzedValueNode {
result.getBase().analyze().getALocalValue() instanceof AbstractGlobalObject
}
override predicate isIncomplete(DataFlow::Incompleteness reason) {
super.isIncomplete(reason)
or
override predicate hasAdditionalIncompleteness(DataFlow::Incompleteness reason) {
clobberedProp(gv, reason)
}
@@ -448,7 +421,7 @@ private AnalyzedVarDef defIn(GlobalVariable gv, TopLevel tl) {
* Holds if there is a write to a property with the same name as `gv` on an object
* for which the analysis is incomplete due to the given `reason`.
*/
pragma[noinline]
cached
private predicate clobberedProp(GlobalVariable gv, DataFlow::Incompleteness reason) {
exists(AnalyzedNode base |
potentialPropWriteOfGlobal(base, gv) and
@@ -456,13 +429,13 @@ private predicate clobberedProp(GlobalVariable gv, DataFlow::Incompleteness reas
)
}
pragma[noinline]
pragma[nomagic]
private predicate indefiniteObjectValue(AbstractValue val, DataFlow::Incompleteness reason) {
val.isIndefinite(reason) and
val.getType() = TTObject()
}
pragma[noinline]
pragma[nomagic]
private predicate potentialPropWriteOfGlobal(AnalyzedNode base, GlobalVariable gv) {
exists(DataFlow::PropWrite pwn |
pwn.getPropertyName() = gv.getName() and
@@ -668,7 +641,7 @@ abstract class FunctionWithAnalyzedParameters extends Function {
* Holds if `p` is a parameter of this function and `arg` is
* the corresponding argument.
*/
abstract predicate argumentPassing(SimpleParameter p, Expr arg);
abstract predicate argumentPassing(Parameter p, Expr arg);
/**
* Holds if `p` is a parameter of this function that may receive a value from an argument.
@@ -688,7 +661,7 @@ abstract private class CallWithAnalyzedParameters extends FunctionWithAnalyzedPa
*/
abstract DataFlow::InvokeNode getAnInvocation();
override predicate argumentPassing(SimpleParameter p, Expr arg) {
override predicate argumentPassing(Parameter p, Expr arg) {
exists(DataFlow::InvokeNode invk, int argIdx | invk = getAnInvocation() |
p = getParameter(argIdx) and
not p.isRestParameter() and

View File

@@ -0,0 +1,291 @@
/**
* Provides classes for working with [Fastify](https://www.fastify.io/) applications.
*/
import javascript
import semmle.javascript.frameworks.HTTP
/**
* Provides classes for working with [Fastify](https://www.fastify.io/) applications.
*/
module Fastify {
/**
* An expression that creates a new Fastify server.
*/
abstract class ServerDefinition extends HTTP::Servers::StandardServerDefinition { }
/**
* A standard way to create a Fastify server.
*/
class StandardServerDefinition extends ServerDefinition {
StandardServerDefinition() {
this = DataFlow::moduleImport("fastify").getAnInvocation().asExpr()
}
}
/**
* A function used as a Fastify route handler.
*
* By default, only handlers installed by a Fastify route setup are recognized,
* but support for other kinds of route handlers can be added by implementing
* additional subclasses of this class.
*/
abstract class RouteHandler extends HTTP::Servers::StandardRouteHandler, DataFlow::ValueNode {
/**
* Gets the parameter of the route handler that contains the request object.
*/
abstract DataFlow::ParameterNode getRequestParameter();
/**
* Gets the parameter of the route handler that contains the reply object.
*/
abstract DataFlow::ParameterNode getReplyParameter();
}
/**
* A Fastify route handler installed by a route setup.
*/
class StandardRouteHandler extends RouteHandler, DataFlow::FunctionNode {
StandardRouteHandler() { this = any(RouteSetup setup).getARouteHandler() }
override DataFlow::ParameterNode getRequestParameter() { result = this.getParameter(0) }
override DataFlow::ParameterNode getReplyParameter() { result = this.getParameter(1) }
}
/**
* A Fastify reply source, that is, the `reply` parameter of a
* route handler.
*/
private class ReplySource extends HTTP::Servers::ResponseSource {
RouteHandler rh;
ReplySource() { this = rh.getReplyParameter() }
/**
* Gets the route handler that provides this response.
*/
override RouteHandler getRouteHandler() { result = rh }
}
/**
* A Fastify request source, that is, the request parameter of a
* route handler.
*/
private class RequestSource extends HTTP::Servers::RequestSource {
RouteHandler rh;
RequestSource() { this = rh.getRequestParameter() }
/**
* Gets the route handler that handles this request.
*/
override RouteHandler getRouteHandler() { result = rh }
}
/**
* A call to a Fastify method that sets up a route.
*/
class RouteSetup extends MethodCallExpr, HTTP::Servers::StandardRouteSetup {
ServerDefinition server;
string methodName;
RouteSetup() {
this.getMethodName() = methodName and
methodName = ["route", "get", "head", "post", "put", "delete", "options", "patch"] and
server.flowsTo(this.getReceiver())
}
override DataFlow::SourceNode getARouteHandler() {
result = getARouteHandler(DataFlow::TypeBackTracker::end())
}
private DataFlow::SourceNode getARouteHandler(DataFlow::TypeBackTracker t) {
t.start() and
result = this.getARouteHandlerExpr().getALocalSource()
or
exists(DataFlow::TypeBackTracker t2 | result = this.getARouteHandler(t2).backtrack(t2, t))
}
override Expr getServer() { result = server }
/** Gets an argument that represents a route handler being registered. */
private DataFlow::Node getARouteHandlerExpr() {
if methodName = "route"
then
result =
this
.flow()
.(DataFlow::MethodCallNode)
.getOptionArgument(0,
["onRequest", "preParsing", "preValidation", "preHandler", "preSerialization",
"onSend", "onResponse", "handler"])
else result = getLastArgument().flow()
}
}
/**
* An access to a user-controlled Fastify request input.
*/
private class RequestInputAccess extends HTTP::RequestInputAccess {
RouteHandler rh;
string kind;
RequestInputAccess() {
exists(string name | this = rh.getARequestSource().ref().getAPropertyRead(name) |
kind = "parameter" and
name = ["params", "query"]
or
kind = "body" and
name = "body"
)
}
override RouteHandler getRouteHandler() { result = rh }
override string getKind() { result = kind }
override predicate isUserControlledObject() {
kind = "body" and
(
usesFastifyPlugin(rh,
DataFlow::moduleImport(["fastify-xml-body-parser", "fastify-formbody"]))
or
usesMiddleware(rh,
any(ExpressLibraries::BodyParser bodyParser | bodyParser.producesUserControlledObjects()))
)
or
kind = "parameter" and
usesFastifyPlugin(rh, DataFlow::moduleImport("fastify-qs"))
}
}
/**
* Holds if `rh` uses `plugin`.
*/
private predicate usesFastifyPlugin(RouteHandler rh, DataFlow::SourceNode plugin) {
exists(RouteSetup setup |
plugin
.flowsTo(setup
.getServer()
.flow()
.(DataFlow::SourceNode)
.getAMethodCall("register")
.getArgument(0)) and // only matches the plugins that apply to all routes
rh = setup.getARouteHandler()
)
}
/**
* Holds if `rh` uses `plugin`.
*/
private predicate usesMiddleware(RouteHandler rh, DataFlow::SourceNode middleware) {
exists(RouteSetup setup |
middleware
.flowsTo(setup
.getServer()
.flow()
.(DataFlow::SourceNode)
.getAMethodCall("use")
.getArgument(0)) and // only matches the middlewares that apply to all routes
rh = setup.getARouteHandler()
)
}
/**
* An access to a header on a Fastify request.
*/
private class RequestHeaderAccess extends HTTP::RequestHeaderAccess {
RouteHandler rh;
RequestHeaderAccess() {
this = rh.getARequestSource().ref().getAPropertyRead("headers").getAPropertyRead()
}
override string getAHeaderName() {
result = this.(DataFlow::PropRead).getPropertyName().toLowerCase()
}
override RouteHandler getRouteHandler() { result = rh }
override string getKind() { result = "header" }
}
/**
* An argument passed to the `send` or `end` method of an HTTP response object.
*/
private class ResponseSendArgument extends HTTP::ResponseSendArgument {
RouteHandler rh;
ResponseSendArgument() {
this = rh.getAResponseSource().ref().getAMethodCall("send").getArgument(0).asExpr()
or
this = rh.(DataFlow::FunctionNode).getAReturn().asExpr()
}
override RouteHandler getRouteHandler() { result = rh }
}
/**
* An invocation of the `redirect` method of an HTTP response object.
*/
private class RedirectInvocation extends HTTP::RedirectInvocation, MethodCallExpr {
RouteHandler rh;
RedirectInvocation() {
this = rh.getAResponseSource().ref().getAMethodCall("redirect").asExpr()
}
override Expr getUrlArgument() { result = this.getLastArgument() }
override RouteHandler getRouteHandler() { result = rh }
}
/**
* An invocation that sets a single header of the HTTP response.
*/
private class SetOneHeader extends HTTP::Servers::StandardHeaderDefinition,
DataFlow::MethodCallNode {
RouteHandler rh;
SetOneHeader() {
this = rh.getAResponseSource().ref().getAMethodCall("header") and
this.getNumArgument() = 2
}
override RouteHandler getRouteHandler() { result = rh }
}
/**
* An invocation that sets any number of headers of the HTTP response.
*/
class SetMultipleHeaders extends HTTP::ExplicitHeaderDefinition, DataFlow::MethodCallNode {
RouteHandler rh;
SetMultipleHeaders() {
this = rh.getAResponseSource().ref().getAMethodCall("headers") and
this.getNumArgument() = 1
}
/**
* Gets a reference to the multiple headers object that is to be set.
*/
private DataFlow::SourceNode getAHeaderSource() { result.flowsTo(this.getArgument(0)) }
override predicate definesExplicitly(string headerName, Expr headerValue) {
exists(string header |
getAHeaderSource().hasPropertyWrite(header, headerValue.flow()) and
headerName = header.toLowerCase()
)
}
override RouteHandler getRouteHandler() { result = rh }
override Expr getNameExpr() {
exists(DataFlow::PropWrite write |
this.getAHeaderSource().flowsTo(write.getBase()) and
result = write.getPropertyNameExpr()
)
}
}
}

View File

@@ -4,3 +4,4 @@ import semmle.javascript.frameworks.Koa
import semmle.javascript.frameworks.NodeJSLib
import semmle.javascript.frameworks.Restify
import semmle.javascript.frameworks.Connect
import semmle.javascript.frameworks.Fastify

View File

@@ -7,18 +7,25 @@ import semmle.javascript.frameworks.HTTP
import semmle.javascript.security.SensitiveActions
module NodeJSLib {
private GlobalVariable processVariable() { variables(result, "process", any(GlobalScope sc)) }
pragma[nomagic]
private GlobalVarAccess processExprInTopLevel(TopLevel tl) {
result = processVariable().getAnAccess() and
tl = result.getTopLevel()
}
pragma[nomagic]
private GlobalVarAccess processExprInNodeModule() {
result = processExprInTopLevel(any(NodeModule m))
}
/**
* An access to the global `process` variable in a Node.js module, interpreted as
* an import of the `process` module.
*/
private class ImplicitProcessImport extends DataFlow::ModuleImportNode::Range {
ImplicitProcessImport() {
exists(GlobalVariable process |
process.getName() = "process" and
this = DataFlow::exprNode(process.getAnAccess())
) and
getTopLevel() instanceof NodeModule
}
ImplicitProcessImport() { this = DataFlow::exprNode(processExprInNodeModule()) }
override string getPath() { result = "process" }
}
@@ -272,7 +279,7 @@ module NodeJSLib {
DataFlow::Node tainted;
PathFlowTarget() {
exists(string methodName | this = DataFlow::moduleMember("path", methodName).getACall() |
exists(string methodName | this = NodeJSLib::Path::moduleMember(methodName).getACall() |
// getters
methodName = "basename" and tainted = getArgument(0)
or
@@ -442,10 +449,7 @@ module NodeJSLib {
private DataFlow::SourceNode fsModule(DataFlow::TypeTracker t) {
exists(string moduleName |
moduleName = "fs" or
moduleName = "graceful-fs" or
moduleName = "fs-extra" or
moduleName = "original-fs"
moduleName = ["mz/fs", "original-fs", "fs-extra", "graceful-fs", "fs"]
|
result = DataFlow::moduleImport(moduleName)
or
@@ -614,6 +618,8 @@ module NodeJSLib {
ChildProcessMethodCall() {
this = maybePromisified(DataFlow::moduleMember("child_process", methodName)).getACall()
or
this = DataFlow::moduleMember("mz/child_process", methodName).getACall()
}
private DataFlow::Node getACommandArgument(boolean shell) {
@@ -710,23 +716,25 @@ module NodeJSLib {
}
/**
* A call to a method from module `vm`
* DEPRECATED Use `VmModuleMemberInvocation` instead.
*/
class VmModuleMethodCall extends DataFlow::CallNode {
string methodName;
deprecated class VmModuleMethodCall = VmModuleMemberInvocation;
VmModuleMethodCall() { this = DataFlow::moduleMember("vm", methodName).getACall() }
/**
* An invocation of a member from module `vm`
*/
class VmModuleMemberInvocation extends DataFlow::InvokeNode {
string memberName;
VmModuleMemberInvocation() { this = DataFlow::moduleMember("vm", memberName).getAnInvocation() }
/**
* Gets the code to be executed as part of this call.
* Gets the code to be executed as part of this invocation.
*/
DataFlow::Node getACodeArgument() {
(
methodName = "runInContext" or
methodName = "runInNewContext" or
methodName = "runInThisContext"
) and
// all of the above methods take the command as their first argument
memberName in ["Script", "SourceTextModule", "compileFunction", "runInContext",
"runInNewContext", "runInThisContext"] and
// all of the above methods/constructors take the command as their first argument
result = getArgument(0)
}
}

View File

@@ -28,39 +28,44 @@ module SQL {
* Provides classes modelling the (API compatible) `mysql` and `mysql2` packages.
*/
private module MySql {
/** Gets the package name `mysql` or `mysql2`. */
string mysql() { result = "mysql" or result = "mysql2" }
private DataFlow::SourceNode mysql() { result = DataFlow::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 reference to a MySQL pool. */
private DataFlow::SourceNode pool() { result = pool(DataFlow::TypeTracker::end()) }
/** Gets a call to `mysql.createConnection`. */
DataFlow::SourceNode createConnection() {
result = DataFlow::moduleMember(mysql(), "createConnection").getACall()
}
DataFlow::CallNode createConnection() { result = mysql().getAMemberCall("createConnection") }
/** Gets a call to `mysql.createPool`. */
DataFlow::SourceNode createPool() {
result = DataFlow::moduleMember(mysql(), "createPool").getACall()
}
/** Gets a data flow node that contains a freshly created MySQL connection instance. */
DataFlow::SourceNode connection() {
result = 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)
)
or
result = createPool().getAMethodCall("getConnection").getCallback(0).getParameter(1)
exists(DataFlow::TypeTracker t2 | result = connection(t2).track(t2, t))
}
/** 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::ValueNode {
override MethodCallExpr astNode;
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [pool(), connection()].getAMethodCall("query") }
QueryCall() {
exists(DataFlow::SourceNode recv | recv = createPool() or recv = connection() |
this = recv.getAMethodCall("query")
)
}
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
}
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
@@ -69,20 +74,11 @@ private module MySql {
}
/** A call to the `escape` or `escapeId` method that performs SQL sanitization. */
class EscapingSanitizer extends SQL::SqlSanitizer, @callexpr {
class EscapingSanitizer extends SQL::SqlSanitizer, MethodCallExpr {
EscapingSanitizer() {
exists(string esc | esc = "escape" or esc = "escapeId" |
exists(DataFlow::SourceNode escape, MethodCallExpr mce |
escape = DataFlow::moduleMember(mysql(), esc) or
escape = connection().getAPropertyRead(esc) or
escape = createPool().getAPropertyRead(esc)
|
this = mce and
mce = escape.getACall().asExpr() and
input = mce.getArgument(0) and
output = mce
)
)
this = [mysql(), pool(), connection()].getAMethodCall(["escape", "escapeId"]).asExpr() and
input = this.getArgument(0) and
output = this
}
}
@@ -91,9 +87,8 @@ private module MySql {
string kind;
Credentials() {
exists(DataFlow::SourceNode call, string prop |
(call = createConnection() or call = createPool()) and
call.asExpr().(CallExpr).hasOptionArgument(0, prop, this) and
exists(string prop |
this = [createConnection(), createPool()].getOptionArgument(0, prop).asExpr() and
(
prop = "user" and kind = "user name"
or
@@ -110,21 +105,8 @@ private module MySql {
* Provides classes modelling the `pg` package.
*/
private module Postgres {
/** Gets an expression of the form `new require('pg').Client()`. */
DataFlow::SourceNode newClient() {
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Client")
}
/** Gets a data flow node that holds a freshly created Postgres client instance. */
DataFlow::SourceNode client() {
result = newClient()
or
// pool.connect(function(err, client) { ... })
result = newPool().getAMethodCall("connect").getCallback(0).getParameter(1)
}
/** Gets an expression that constructs a new connection pool. */
DataFlow::SourceNode newPool() {
DataFlow::InvokeNode newPool() {
// new require('pg').Pool()
result = DataFlow::moduleImport("pg").getAConstructorInvocation("Pool")
or
@@ -132,26 +114,42 @@ private module Postgres {
result = DataFlow::moduleImport("pg-pool").getAnInstantiation()
}
private DataFlow::SourceNode clientOrPool(DataFlow::TypeTracker t) {
/** Gets a data flow node referring to a connection pool. */
private DataFlow::SourceNode pool(DataFlow::TypeTracker t) {
t.start() and
(result = client() or result = newPool())
result = newPool()
or
exists(DataFlow::TypeTracker t2 | result = clientOrPool(t2).track(t2, t))
exists(DataFlow::TypeTracker t2 | result = pool(t2).track(t2, t))
}
private DataFlow::SourceNode clientOrPool() {
result = clientOrPool(DataFlow::TypeTracker::end())
/** 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")
}
/** 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)
)
or
exists(DataFlow::TypeTracker t2 | result = client(t2).track(t2, t))
}
/** 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::ValueNode {
override MethodCallExpr astNode;
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = [client(), pool()].getAMethodCall("query") }
QueryCall() { this = clientOrPool().getAMethodCall("query") }
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
}
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
@@ -164,14 +162,10 @@ private module Postgres {
string kind;
Credentials() {
exists(DataFlow::InvokeNode call, string prop |
(call = newClient() or call = newPool()) and
this = call.getOptionArgument(0, prop).asExpr() and
(
prop = "user" and kind = "user name"
or
prop = "password" and kind = prop
)
exists(string prop | this = [newClient(), newPool()].getOptionArgument(0, prop).asExpr() |
prop = "user" and kind = "user name"
or
prop = "password" and kind = prop
)
}
@@ -196,10 +190,19 @@ private module Sqlite {
result = sqlite().getAConstructorInvocation("Database")
}
/** A call to a Sqlite query method. */
private class QueryCall extends DatabaseAccess, DataFlow::ValueNode {
override MethodCallExpr astNode;
/** 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() {
exists(string meth |
meth = "all" or
@@ -209,13 +212,11 @@ private module Sqlite {
meth = "prepare" or
meth = "run"
|
this = newDb().getAMethodCall(meth)
this = db().getAMethodCall(meth)
)
}
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
}
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
/** An expression that is passed to the `query` method and hence interpreted as SQL. */
@@ -229,17 +230,25 @@ private module Sqlite {
*/
private module MsSql {
/** Gets a reference to the `mssql` module. */
DataFlow::ModuleImportNode mssql() { result.getPath() = "mssql" }
DataFlow::SourceNode mssql() { result = DataFlow::moduleImport("mssql") }
/** Gets an expression that creates a request object. */
DataFlow::SourceNode request() {
// new require('mssql').Request()
result = mssql().getAConstructorInvocation("Request")
/** 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")
)
or
// request.input(...)
result = request().getAMethodCall("input")
exists(DataFlow::TypeTracker t2 | result = request(t2).track(t2, t))
}
/** 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;
@@ -252,16 +261,10 @@ private module MsSql {
}
/** A call to a MsSql query method. */
private class QueryCall extends DatabaseAccess, DataFlow::ValueNode {
override MethodCallExpr astNode;
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = request().getAMethodCall(["query", "batch"]) }
QueryCall() {
exists(string meth | this = request().getAMethodCall(meth) | meth = "query" or meth = "batch")
}
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
}
override DataFlow::Node getAQueryArgument() { result = getArgument(0) }
}
/** An expression that is passed to a method that interprets it as SQL. */
@@ -310,17 +313,22 @@ private module MsSql {
* Provides classes modelling the `sequelize` package.
*/
private module Sequelize {
/** Gets an import of the `sequelize` module. */
DataFlow::ModuleImportNode sequelize() { result.getPath() = "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 expression that creates an instance of the `Sequelize` class. */
DataFlow::SourceNode newSequelize() { result = sequelize().getAnInstantiation() }
/** Gets a node referring to an instance of the `Sequelize` class. */
DataFlow::SourceNode sequelize() { result = sequelize(DataFlow::TypeTracker::end()) }
/** A call to `Sequelize.query`. */
private class QueryCall extends DatabaseAccess, DataFlow::ValueNode {
override MethodCallExpr astNode;
QueryCall() { this = newSequelize().getAMethodCall("query") }
QueryCall() { this = sequelize().getAMethodCall("query") }
override DataFlow::Node getAQueryArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
@@ -341,7 +349,7 @@ private module Sequelize {
Credentials() {
exists(NewExpr ne, string prop |
ne = newSequelize().asExpr() and
ne = sequelize().asExpr() and
(
this = ne.getArgument(1) and prop = "username"
or
@@ -376,27 +384,61 @@ private module Spanner {
result = DataFlow::moduleMember("@google-cloud/spanner", "Spanner")
}
/**
* Gets a node that refers to an instance of the `Database` class.
*/
DataFlow::SourceNode database() {
result = spanner().getAnInvocation().getAMethodCall("instance").getAMethodCall("database")
/** 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))
}
/**
* Gets a node that refers to an instance of the `v1.SpannerClient` class.
*/
DataFlow::SourceNode v1SpannerClient() {
/** 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. */
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 a transaction object.
*/
DataFlow::SourceNode transaction() {
result = database().getAMethodCall("runTransaction").getCallback(0).getParameter(1)
/** 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. */
DataFlow::SourceNode transaction() { result = transaction(DataFlow::TypeTracker::end()) }
/**
* A call to a Spanner method that executes a SQL query.
*/
@@ -418,9 +460,7 @@ private module Spanner {
*/
class DatabaseRunCall extends SqlExecution {
DatabaseRunCall() {
exists(string run | run = "run" or run = "runPartitionedUpdate" or run = "runStream" |
this = database().getAMethodCall(run)
)
this = database().getAMethodCall(["run", "runPartitionedUpdate", "runStream"])
}
}
@@ -428,11 +468,7 @@ private module Spanner {
* A call to `Transaction.run`, `Transaction.runStream` or `Transaction.runUpdate`.
*/
class TransactionRunCall extends SqlExecution {
TransactionRunCall() {
exists(string run | run = "run" or run = "runStream" or run = "runUpdate" |
this = transaction().getAMethodCall(run)
)
}
TransactionRunCall() { this = transaction().getAMethodCall(["run", "runStream", "runUpdate"]) }
}
/**
@@ -440,9 +476,7 @@ private module Spanner {
*/
class ExecuteSqlCall extends SqlExecution {
ExecuteSqlCall() {
exists(string exec | exec = "executeSql" or exec = "executeStreamingSql" |
this = v1SpannerClient().getAMethodCall(exec)
)
this = v1SpannerClient().getAMethodCall(["executeSql", "executeStreamingSql"])
}
override DataFlow::Node getAQueryArgument() {

View File

@@ -51,13 +51,9 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
)
or
shell = true and
(
mod = "exec" and
optionsArg = -2 and
cmdArg = 0
or
mod = "remote-exec" and cmdArg = 1 and optionsArg = -1
)
mod = "exec" and
optionsArg = -2 and
cmdArg = 0
) and
callee = DataFlow::moduleImport(mod)
|
@@ -97,3 +93,33 @@ private boolean getSync(string name) {
then result = true
else result = false
}
private class RemoteCommandExecutor extends SystemCommandExecution, DataFlow::InvokeNode {
int cmdArg;
RemoteCommandExecutor() {
this = DataFlow::moduleImport("remote-exec").getACall() 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
cmdArg = 0
)
or
exists(DataFlow::SourceNode ssh2stream |
ssh2stream = DataFlow::moduleMember("ssh2-streams", "SSH2Stream") and
this = ssh2stream.getAnInstantiation().getAMethodCall("exec") and
cmdArg = 1
)
}
override DataFlow::Node getACommandArgument() { result = getArgument(cmdArg) }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
override predicate isSync() { none() }
override DataFlow::Node getOptionsArg() { none() }
}

View File

@@ -48,7 +48,9 @@ module CodeInjection {
* `vm` module.
*/
class NodeJSVmSink extends Sink, DataFlow::ValueNode {
NodeJSVmSink() { exists(NodeJSLib::VmModuleMethodCall call | this = call.getACodeArgument()) }
NodeJSVmSink() {
exists(NodeJSLib::VmModuleMemberInvocation inv | this = inv.getACodeArgument())
}
}
/**

View File

@@ -24,6 +24,10 @@ module DomBasedXss {
node instanceof Sanitizer
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof SanitizerGuard
}
override predicate isAdditionalLoadStoreStep(
DataFlow::Node pred, DataFlow::Node succ, string predProp, string succProp
) {
@@ -47,6 +51,10 @@ module DomBasedXss {
prop = urlSuffixPseudoProperty()
)
}
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
}
}
private string urlSuffixPseudoProperty() { result = "$UrlSuffix$" }

View File

@@ -51,8 +51,11 @@ module IncompleteHtmlAttributeSanitization {
string lhs;
HtmlAttributeConcatenation() {
lhs = this.getPreviousLeaf().getStringValue().regexpCapture("(.*)=\"[^\"]*", 1) and
this.getNextLeaf().getStringValue().regexpMatch(".*\".*")
lhs = this.getPreviousLeaf().getStringValue().regexpCapture("(?s)(.*)=\"[^\"]*", 1) and
(
this.getNextLeaf().getStringValue().regexpMatch(".*\".*") or
this instanceof StringOps::HtmlConcatenationLeaf
)
}
/**

View File

@@ -52,7 +52,7 @@ private DataFlow::SourceNode argumentList(SystemCommandExecution sys, DataFlow::
result = pred.backtrack(t2, t)
or
t = t2.continue() and
TaintTracking::arrayFunctionTaintStep(result, pred, _)
TaintTracking::arrayFunctionTaintStep(any(DataFlow::Node n | result.flowsTo(n)), pred, _)
)
}

View File

@@ -22,5 +22,9 @@ module ReflectedXss {
super.isSanitizer(node) or
node instanceof Sanitizer
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof SanitizerGuard
}
}
}

View File

@@ -22,6 +22,10 @@ module StoredXss {
super.isSanitizer(node) or
node instanceof Sanitizer
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof SanitizerGuard
}
}
/** A file name, considered as a flow source for stored XSS. */

View File

@@ -32,227 +32,14 @@ module TaintedPath {
}
override predicate isBarrierGuard(DataFlow::BarrierGuardNode guard) {
guard instanceof StartsWithDotDotSanitizer or
guard instanceof StartsWithDirSanitizer or
guard instanceof IsAbsoluteSanitizer or
guard instanceof ContainsDotDotSanitizer or
guard instanceof RelativePathStartsWithSanitizer or
guard instanceof IsInsideCheckSanitizer
guard instanceof BarrierGuardNode
}
override predicate isAdditionalFlowStep(
DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel,
DataFlow::FlowLabel dstlabel
) {
isTaintedPathStep(src, dst, srclabel, dstlabel)
or
// Ignore all preliminary sanitization after decoding URI components
srclabel instanceof Label::PosixPath and
dstlabel instanceof Label::PosixPath and
(
any(UriLibraryStep step).step(src, dst)
or
exists(DataFlow::CallNode decode |
decode.getCalleeName() = "decodeURIComponent" or decode.getCalleeName() = "decodeURI"
|
src = decode.getArgument(0) and
dst = decode
)
)
or
promiseTaintStep(src, dst) and srclabel = dstlabel
or
any(TaintTracking::PersistentStorageTaintStep st).step(src, dst) and srclabel = dstlabel
or
exists(DataFlow::PropRead read | read = dst |
src = read.getBase() and
read.getPropertyName() != "length" and
srclabel = dstlabel
)
or
// string method calls of interest
exists(DataFlow::MethodCallNode mcn, string name |
srclabel = dstlabel and dst = mcn and mcn.calls(src, name)
|
exists(string substringMethodName |
substringMethodName = "substr" or
substringMethodName = "substring" or
substringMethodName = "slice"
|
name = substringMethodName and
// to avoid very dynamic transformations, require at least one fixed index
exists(mcn.getAnArgument().asExpr().getIntValue())
)
or
exists(string argumentlessMethodName |
argumentlessMethodName = "toLocaleLowerCase" or
argumentlessMethodName = "toLocaleUpperCase" or
argumentlessMethodName = "toLowerCase" or
argumentlessMethodName = "toUpperCase" or
argumentlessMethodName = "trim" or
argumentlessMethodName = "trimLeft" or
argumentlessMethodName = "trimRight"
|
name = argumentlessMethodName
)
)
or
// A `str.split()` call can either split into path elements (`str.split("/")`) or split by some other string.
exists(StringSplitCall mcn | dst = mcn and mcn.getBaseString() = src |
if mcn.getSeparator() = "/"
then
srclabel.(Label::PosixPath).canContainDotDotSlash() and
dstlabel instanceof Label::SplitPath
else srclabel = dstlabel
)
or
// array method calls of interest
exists(DataFlow::MethodCallNode mcn, string name | dst = mcn and mcn.calls(src, name) |
(
name = "pop" or
name = "shift"
) and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
or
(
name = "slice" or
name = "splice" or
name = "concat"
) and
dstlabel instanceof Label::SplitPath and
srclabel instanceof Label::SplitPath
or
name = "join" and
mcn.getArgument(0).mayHaveStringValue("/") and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
)
or
// prefix.concat(path)
exists(DataFlow::MethodCallNode mcn |
mcn.getMethodName() = "concat" and mcn.getAnArgument() = src
|
dst = mcn and
dstlabel instanceof Label::SplitPath and
srclabel instanceof Label::SplitPath
)
or
// reading unknown property of split path
exists(DataFlow::PropRead read | read = dst |
src = read.getBase() and
not read.getPropertyName() = "length" and
not exists(read.getPropertyNameExpr().getIntValue()) and
// split[split.length - 1]
not exists(BinaryExpr binop |
read.getPropertyNameExpr() = binop and
binop.getAnOperand().getIntValue() = 1 and
binop.getAnOperand().(PropAccess).getPropertyName() = "length"
) and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
)
}
/**
* Holds if we should include a step from `src -> dst` with labels `srclabel -> dstlabel`, and the
* standard taint step `src -> dst` should be suppresesd.
*/
predicate isTaintedPathStep(
DataFlow::Node src, DataFlow::Node dst, Label::PosixPath srclabel, Label::PosixPath dstlabel
) {
// path.normalize() and similar
exists(NormalizingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel = srclabel.toNormalized()
)
or
// path.resolve() and similar
exists(ResolvingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel.isAbsolute() and
dstlabel.isNormalized()
)
or
// path.relative() and similar
exists(NormalizingRelativePathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel.isRelative() and
dstlabel.isNormalized()
)
or
// path.dirname() and similar
exists(PreservingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
srclabel = dstlabel
)
or
// foo.replace(/\./, "") and similar
exists(DotRemovingReplaceCall call |
src = call.getInput() and
dst = call.getOutput() and
srclabel.isAbsolute() and
dstlabel.isAbsolute() and
dstlabel.isNormalized()
)
or
// foo.replace(/(\.\.\/)*/, "") and similar
exists(DotDotSlashPrefixRemovingReplace call |
src = call.getInput() and
dst = call.getOutput()
|
// the 4 possible combinations of normalized + relative for `srclabel`, and the possible values for `dstlabel` in each case.
srclabel.isNonNormalized() and srclabel.isRelative() // raw + relative -> any()
or
srclabel.isNormalized() and srclabel.isAbsolute() and srclabel = dstlabel // normalized + absolute -> normalized + absolute
or
srclabel.isNonNormalized() and srclabel.isAbsolute() and dstlabel.isAbsolute() // raw + absolute -> raw/normalized + absolute
// normalized + relative -> none()
)
or
// path.join()
exists(DataFlow::CallNode join, int n |
join = NodeJSLib::Path::moduleMember("join").getACall()
|
src = join.getArgument(n) and
dst = join and
(
// If the initial argument is tainted, just normalize it. It can be relative or absolute.
n = 0 and
dstlabel = srclabel.toNormalized()
or
// For later arguments, the flow label depends on whether the first argument is absolute or relative.
// If in doubt, we assume it is absolute.
n > 0 and
srclabel.canContainDotDotSlash() and
dstlabel.isNormalized() and
if isRelative(join.getArgument(0).getStringValue())
then dstlabel.isRelative()
else dstlabel.isAbsolute()
)
)
or
// String concatenation - behaves like path.join() except without normalization
exists(DataFlow::Node operator, int n |
StringConcatenation::taintStep(src, dst, operator, n)
|
// use ordinary taint flow for the first operand
n = 0 and
srclabel = dstlabel
or
n > 0 and
srclabel.canContainDotDotSlash() and
dstlabel.isNonNormalized() and // The ../ is no longer at the beginning of the string.
(
if isRelative(StringConcatenation::getOperand(operator, 0).getStringValue())
then dstlabel.isRelative()
else dstlabel.isAbsolute()
)
)
isAdditionalTaintedPathFlowStep(src, dst, srclabel, dstlabel)
}
}
}

View File

@@ -28,6 +28,11 @@ module TaintedPath {
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A barrier guard for tainted-path vulnerabilities.
*/
abstract class BarrierGuardNode extends DataFlow::LabeledBarrierGuardNode { }
module Label {
/**
* A string indicating if a path is normalized, that is, whether internal `../` components
@@ -343,7 +348,7 @@ module TaintedPath {
*
* This is relevant for paths that are known to be normalized.
*/
class StartsWithDotDotSanitizer extends DataFlow::LabeledBarrierGuardNode {
class StartsWithDotDotSanitizer extends BarrierGuardNode {
StringOps::StartsWith startsWith;
StartsWithDotDotSanitizer() {
@@ -369,7 +374,7 @@ module TaintedPath {
* A check of form `x.startsWith(dir)` that sanitizes normalized absolute paths, since it is then
* known to be in a subdirectory of `dir`.
*/
class StartsWithDirSanitizer extends DataFlow::LabeledBarrierGuardNode {
class StartsWithDirSanitizer extends BarrierGuardNode {
StringOps::StartsWith startsWith;
StartsWithDirSanitizer() {
@@ -393,7 +398,7 @@ module TaintedPath {
* A call to `path.isAbsolute` as a sanitizer for relative paths in true branch,
* and a sanitizer for absolute paths in the false branch.
*/
class IsAbsoluteSanitizer extends DataFlow::LabeledBarrierGuardNode {
class IsAbsoluteSanitizer extends BarrierGuardNode {
DataFlow::Node operand;
boolean polarity;
boolean negatable;
@@ -429,7 +434,7 @@ module TaintedPath {
/**
* An expression of form `x.includes("..")` or similar.
*/
class ContainsDotDotSanitizer extends DataFlow::LabeledBarrierGuardNode {
class ContainsDotDotSanitizer extends BarrierGuardNode {
StringOps::Includes contains;
ContainsDotDotSanitizer() {
@@ -465,7 +470,7 @@ module TaintedPath {
* }
* ```
*/
class RelativePathStartsWithSanitizer extends DataFlow::BarrierGuardNode {
class RelativePathStartsWithSanitizer extends BarrierGuardNode {
StringOps::StartsWith startsWith;
DataFlow::CallNode pathCall;
string member;
@@ -507,7 +512,7 @@ module TaintedPath {
* An expression of form `isInside(x, y)` or similar, where `isInside` is
* a library check for the relation between `x` and `y`.
*/
class IsInsideCheckSanitizer extends DataFlow::LabeledBarrierGuardNode {
class IsInsideCheckSanitizer extends BarrierGuardNode {
DataFlow::Node checked;
boolean onlyNormalizedAbsolutePaths;
@@ -614,4 +619,220 @@ module TaintedPath {
class SendPathSink extends Sink, DataFlow::ValueNode {
SendPathSink() { this = DataFlow::moduleImport("send").getACall().getArgument(1) }
}
/**
* Holds if there is a step `src -> dst` mapping `srclabel` to `dstlabel` relevant for path traversal vulnerabilities.
*/
predicate isAdditionalTaintedPathFlowStep(
DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel,
DataFlow::FlowLabel dstlabel
) {
isPosixPathStep(src, dst, srclabel, dstlabel)
or
// Ignore all preliminary sanitization after decoding URI components
srclabel instanceof Label::PosixPath and
dstlabel instanceof Label::PosixPath and
(
any(UriLibraryStep step).step(src, dst)
or
exists(DataFlow::CallNode decode |
decode.getCalleeName() = "decodeURIComponent" or decode.getCalleeName() = "decodeURI"
|
src = decode.getArgument(0) and
dst = decode
)
)
or
promiseTaintStep(src, dst) and srclabel = dstlabel
or
any(TaintTracking::PersistentStorageTaintStep st).step(src, dst) and srclabel = dstlabel
or
exists(DataFlow::PropRead read | read = dst |
src = read.getBase() and
read.getPropertyName() != "length" and
srclabel = dstlabel
)
or
// string method calls of interest
exists(DataFlow::MethodCallNode mcn, string name |
srclabel = dstlabel and dst = mcn and mcn.calls(src, name)
|
exists(string substringMethodName |
substringMethodName = "substr" or
substringMethodName = "substring" or
substringMethodName = "slice"
|
name = substringMethodName and
// to avoid very dynamic transformations, require at least one fixed index
exists(mcn.getAnArgument().asExpr().getIntValue())
)
or
exists(string argumentlessMethodName |
argumentlessMethodName = "toLocaleLowerCase" or
argumentlessMethodName = "toLocaleUpperCase" or
argumentlessMethodName = "toLowerCase" or
argumentlessMethodName = "toUpperCase" or
argumentlessMethodName = "trim" or
argumentlessMethodName = "trimLeft" or
argumentlessMethodName = "trimRight"
|
name = argumentlessMethodName
)
)
or
// A `str.split()` call can either split into path elements (`str.split("/")`) or split by some other string.
exists(StringSplitCall mcn | dst = mcn and mcn.getBaseString() = src |
if mcn.getSeparator() = "/"
then
srclabel.(Label::PosixPath).canContainDotDotSlash() and
dstlabel instanceof Label::SplitPath
else srclabel = dstlabel
)
or
// array method calls of interest
exists(DataFlow::MethodCallNode mcn, string name | dst = mcn and mcn.calls(src, name) |
(
name = "pop" or
name = "shift"
) and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
or
(
name = "slice" or
name = "splice" or
name = "concat"
) and
dstlabel instanceof Label::SplitPath and
srclabel instanceof Label::SplitPath
or
name = "join" and
mcn.getArgument(0).mayHaveStringValue("/") and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
)
or
// prefix.concat(path)
exists(DataFlow::MethodCallNode mcn |
mcn.getMethodName() = "concat" and mcn.getAnArgument() = src
|
dst = mcn and
dstlabel instanceof Label::SplitPath and
srclabel instanceof Label::SplitPath
)
or
// reading unknown property of split path
exists(DataFlow::PropRead read | read = dst |
src = read.getBase() and
not read.getPropertyName() = "length" and
not exists(read.getPropertyNameExpr().getIntValue()) and
// split[split.length - 1]
not exists(BinaryExpr binop |
read.getPropertyNameExpr() = binop and
binop.getAnOperand().getIntValue() = 1 and
binop.getAnOperand().(PropAccess).getPropertyName() = "length"
) and
srclabel instanceof Label::SplitPath and
dstlabel.(Label::PosixPath).canContainDotDotSlash()
)
}
/**
* Holds if we should include a step from `src -> dst` with labels `srclabel -> dstlabel`, and the
* standard taint step `src -> dst` should be suppresesd.
*/
private predicate isPosixPathStep(
DataFlow::Node src, DataFlow::Node dst, Label::PosixPath srclabel, Label::PosixPath dstlabel
) {
// path.normalize() and similar
exists(NormalizingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel = srclabel.toNormalized()
)
or
// path.resolve() and similar
exists(ResolvingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel.isAbsolute() and
dstlabel.isNormalized()
)
or
// path.relative() and similar
exists(NormalizingRelativePathCall call |
src = call.getInput() and
dst = call.getOutput() and
dstlabel.isRelative() and
dstlabel.isNormalized()
)
or
// path.dirname() and similar
exists(PreservingPathCall call |
src = call.getInput() and
dst = call.getOutput() and
srclabel = dstlabel
)
or
// foo.replace(/\./, "") and similar
exists(DotRemovingReplaceCall call |
src = call.getInput() and
dst = call.getOutput() and
srclabel.isAbsolute() and
dstlabel.isAbsolute() and
dstlabel.isNormalized()
)
or
// foo.replace(/(\.\.\/)*/, "") and similar
exists(DotDotSlashPrefixRemovingReplace call |
src = call.getInput() and
dst = call.getOutput()
|
// the 4 possible combinations of normalized + relative for `srclabel`, and the possible values for `dstlabel` in each case.
srclabel.isNonNormalized() and srclabel.isRelative() // raw + relative -> any()
or
srclabel.isNormalized() and srclabel.isAbsolute() and srclabel = dstlabel // normalized + absolute -> normalized + absolute
or
srclabel.isNonNormalized() and srclabel.isAbsolute() and dstlabel.isAbsolute() // raw + absolute -> raw/normalized + absolute
// normalized + relative -> none()
)
or
// path.join()
exists(DataFlow::CallNode join, int n |
join = NodeJSLib::Path::moduleMember("join").getACall()
|
src = join.getArgument(n) and
dst = join and
(
// If the initial argument is tainted, just normalize it. It can be relative or absolute.
n = 0 and
dstlabel = srclabel.toNormalized()
or
// For later arguments, the flow label depends on whether the first argument is absolute or relative.
// If in doubt, we assume it is absolute.
n > 0 and
srclabel.canContainDotDotSlash() and
dstlabel.isNormalized() and
if isRelative(join.getArgument(0).getStringValue())
then dstlabel.isRelative()
else dstlabel.isAbsolute()
)
)
or
// String concatenation - behaves like path.join() except without normalization
exists(DataFlow::Node operator, int n | StringConcatenation::taintStep(src, dst, operator, n) |
// use ordinary taint flow for the first operand
n = 0 and
srclabel = dstlabel
or
n > 0 and
srclabel.canContainDotDotSlash() and
dstlabel.isNonNormalized() and // The ../ is no longer at the beginning of the string.
(
if isRelative(StringConcatenation::getOperand(operator, 0).getStringValue())
then dstlabel.isRelative()
else dstlabel.isAbsolute()
)
)
}
}

View File

@@ -0,0 +1,35 @@
/**
* Provides a taint tracking configuration for reasoning about shell command
* constructed from library input vulnerabilities (CWE-078).
*
* Note, for performance reasons: only import this file if
* `UnsafeShellCommandConstruction::Configuration` is needed, otherwise
* `UnsafeShellCommandConstructionCustomizations` should be imported instead.
*/
import javascript
/**
* Classes and predicates for the shell command constructed from library input query.
*/
module UnsafeShellCommandConstruction {
import UnsafeShellCommandConstructionCustomizations::UnsafeShellCommandConstruction
/**
* A taint-tracking configuration for reasoning about shell command constructed from library input vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "UnsafeShellCommandConstruction" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof PathExistsSanitizerGuard or
guard instanceof TaintTracking::AdHocWhitelistCheckSanitizer
}
}
}

View File

@@ -0,0 +1,190 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* shell command constructed from library input vulnerabilities,
* as well as extension points for adding your own.
*/
import javascript
private import semmle.javascript.security.dataflow.RemoteFlowSources
private import semmle.javascript.PackageExports as Exports
/**
* Module containing sources, sinks, and sanitizers for shell command constructed from library input.
*/
module UnsafeShellCommandConstruction {
import IndirectCommandArgument
import semmle.javascript.security.IncompleteBlacklistSanitizer as IncompleteBlacklistSanitizer
/**
* A data flow source for shell command constructed from library input.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for shell command constructed from library input.
*/
abstract class Sink extends DataFlow::Node {
/**
* Gets a description how the shell command is constructed for this sink.
*/
abstract string getSinkType();
/**
* Gets the dataflow node that executes the shell command.
*/
abstract SystemCommandExecution getCommandExecution();
/**
* Gets the node that should be highlighted for this sink.
* E.g. for a string concatenation, the sink is one of the leaves and the highlight is the concatenation root.
*/
abstract DataFlow::Node getAlertLocation();
}
/**
* A sanitizer for shell command constructed from library input.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A parameter of an exported function, seen as a source for shell command constructed from library input.
*/
class ExternalInputSource extends Source, DataFlow::ParameterNode {
ExternalInputSource() {
this =
Exports::getAValueExportedBy(Exports::getTopmostPackageJSON())
.(DataFlow::FunctionNode)
.getAParameter() and
not this.getName() = ["cmd", "command"] // looks to be on purpose.
}
}
/**
* Gets a node that is later executed as a shell command in the command execution `sys`.
*/
private DataFlow::Node isExecutedAsShellCommand(
DataFlow::TypeBackTracker t, SystemCommandExecution sys
) {
t.start() and result = sys.getACommandArgument() and sys.isShellInterpreted(result)
or
t.start() and isIndirectCommandArgument(result, sys)
or
exists(DataFlow::TypeBackTracker t2 |
t2 = t.smallstep(result, isExecutedAsShellCommand(t2, sys))
)
}
/**
* A string concatenation that is later executed as a shell command.
*/
class StringConcatEndingInCommandExecutionSink extends Sink, StringOps::ConcatenationLeaf {
SystemCommandExecution sys;
StringOps::ConcatenationRoot root;
StringConcatEndingInCommandExecutionSink() {
this = root.getALeaf() and
root = isExecutedAsShellCommand(DataFlow::TypeBackTracker::end(), sys) and
exists(string prev | prev = this.getPreviousLeaf().getStringValue() |
prev.regexpMatch(".* ('|\")?[0-9a-zA-Z/]*")
)
}
override string getSinkType() { result = "String concatenation" }
override SystemCommandExecution getCommandExecution() { result = sys }
override DataFlow::Node getAlertLocation() { result = root }
}
/**
* An element pushed to an array, where the array is later used to execute a shell command.
*/
class ArrayAppendEndingInCommandExecutinSink extends Sink {
DataFlow::SourceNode array;
SystemCommandExecution sys;
ArrayAppendEndingInCommandExecutinSink() {
this =
[array.(DataFlow::ArrayCreationNode).getAnElement(),
array.getAMethodCall(["push", "unshift"]).getAnArgument()] and
exists(DataFlow::MethodCallNode joinCall | array.getAMethodCall("join") = joinCall |
joinCall = isExecutedAsShellCommand(DataFlow::TypeBackTracker::end(), sys) and
joinCall.getNumArgument() = 1 and
joinCall.getArgument(0).getStringValue() = " "
)
}
override string getSinkType() { result = "Array element" }
override SystemCommandExecution getCommandExecution() { result = sys }
override DataFlow::Node getAlertLocation() { result = this }
}
/**
* A formatted string that is later executed as a shell command.
*/
class FormatedStringInCommandExecutionSink extends Sink {
PrintfStyleCall call;
SystemCommandExecution sys;
FormatedStringInCommandExecutionSink() {
this = call.getFormatArgument(_) and
call = isExecutedAsShellCommand(DataFlow::TypeBackTracker::end(), sys) and
exists(string formatString | call.getFormatString().mayHaveStringValue(formatString) |
formatString.regexpMatch(".* ('|\")?[0-9a-zA-Z/]*%.*")
)
}
override string getSinkType() { result = "Formatted string" }
override SystemCommandExecution getCommandExecution() { result = sys }
override DataFlow::Node getAlertLocation() { result = this }
}
/**
* A sanitizer like: "'"+name.replace(/'/g,"'\\''")+"'"
* Which sanitizes on Unix.
* The sanitizer is only safe if sorounded by single-quotes, which is assumed.
*/
class ReplaceQuotesSanitizer extends Sanitizer, StringReplaceCall {
ReplaceQuotesSanitizer() {
this.getAReplacedString() = "'" and
this.isGlobal() and
this.getRawReplacement().mayHaveStringValue(["'\\''", ""])
}
}
/**
* A chain of replace calls that replaces all unsafe chars for shell-commands.
*/
class ChainSanitizer extends Sanitizer, IncompleteBlacklistSanitizer::StringReplaceCallSequence {
ChainSanitizer() {
forall(string char |
char = ["&", "`", "$", "|", ">", "<", "#", ";", "(", ")", "[", "]", "\n"]
|
this.getAMember().getAReplacedString() = char
)
}
}
/**
* A sanitizer that sanitizers paths that exist in the file-system.
* For example: `x` is sanitized in `fs.existsSync(x)` or `fs.existsSync(x + "/suffix/path")`.
*/
class PathExistsSanitizerGuard extends TaintTracking::SanitizerGuardNode, DataFlow::CallNode {
PathExistsSanitizerGuard() {
this = DataFlow::moduleMember("path", "exist").getACall() or
this = DataFlow::moduleMember("fs", "existsSync").getACall()
}
override predicate sanitizes(boolean outcome, Expr e) {
outcome = true and
(
e = getArgument(0).asExpr() or
e = getArgument(0).(StringOps::ConcatenationRoot).getALeaf().asExpr()
)
}
}
}

View File

@@ -23,6 +23,9 @@ module Shared {
/** A sanitizer for XSS vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }
/** A sanitizer guard for XSS vulnerabilities. */
abstract class SanitizerGuard extends TaintTracking::SanitizerGuardNode { }
/**
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
* XSS vulnerabilities.
@@ -51,6 +54,85 @@ module Shared {
)
}
}
private import semmle.javascript.security.dataflow.IncompleteHtmlAttributeSanitizationCustomizations::IncompleteHtmlAttributeSanitization as IncompleteHTML
/**
* A guard that checks if a string can contain quotes, which is a guard for strings that are inside a HTML attribute.
*/
class QuoteGuard extends SanitizerGuard, StringOps::Includes {
QuoteGuard() {
this.getSubstring().mayHaveStringValue("\"") and
this
.getBaseString()
.getALocalSource()
.flowsTo(any(IncompleteHTML::HtmlAttributeConcatenation attributeConcat))
}
override predicate sanitizes(boolean outcome, Expr e) {
e = this.getBaseString().getEnclosingExpr() and outcome = this.getPolarity().booleanNot()
}
}
/**
* A sanitizer guard that checks for the existence of HTML chars in a string.
* E.g. `/["'&<>]/.exec(str)`.
*/
class ContainsHTMLGuard extends SanitizerGuard, DataFlow::MethodCallNode {
DataFlow::RegExpCreationNode regExp;
ContainsHTMLGuard() {
this.getMethodName() = ["test", "exec"] and
this.getReceiver().getALocalSource() = regExp and
regExp.getRoot() instanceof RegExpCharacterClass and
forall(string s | s = ["\"", "&", "<", ">"] | regExp.getRoot().getAMatchedString() = s)
}
override predicate sanitizes(boolean outcome, Expr e) {
outcome = false and e = this.getArgument(0).asExpr()
}
}
/**
* Holds if `str` is used in a switch-case that has cases matching HTML escaping.
*/
private predicate isUsedInHTMLEscapingSwitch(Expr str) {
exists(SwitchStmt switch |
// "\"".charCodeAt(0) == 34, "&".charCodeAt(0) == 38, "<".charCodeAt(0) == 60
forall(int c | c = [34, 38, 60] | c = switch.getACase().getExpr().getIntValue()) and
exists(DataFlow::MethodCallNode mcn | mcn.getMethodName() = "charCodeAt" |
mcn.flowsToExpr(switch.getExpr()) and
str = mcn.getReceiver().asExpr()
)
or
forall(string c | c = ["\"", "&", "<"] | c = switch.getACase().getExpr().getStringValue()) and
(
exists(DataFlow::MethodCallNode mcn | mcn.getMethodName() = "charAt" |
mcn.flowsToExpr(switch.getExpr()) and
str = mcn.getReceiver().asExpr()
)
or
exists(DataFlow::PropRead read | exists(read.getPropertyNameExpr()) |
read.flowsToExpr(switch.getExpr()) and
str = read.getBase().asExpr()
)
)
)
}
/**
* Gets an Ssa variable that is used in a sanitizing switch statement.
* The `pragma[noinline]` is to avoid materializing a cartesian product.
*/
pragma[noinline]
private SsaVariable getAPathEscapedInSwitch() { isUsedInHTMLEscapingSwitch(result.getAUse()) }
/**
* An expression that is sanitized by a switch-case.
*/
class IsEscapedInSwitchSanitizer extends Sanitizer {
IsEscapedInSwitchSanitizer() { this.asExpr() = getAPathEscapedInSwitch().getAUse() }
}
}
/** Provides classes and predicates for the DOM-based XSS query. */
@@ -64,6 +146,9 @@ module DomBasedXss {
/** A sanitizer for DOM-based XSS vulnerabilities. */
abstract class Sanitizer extends Shared::Sanitizer { }
/** A sanitizer guard for DOM-based XSS vulnerabilities. */
abstract class SanitizerGuard extends Shared::SanitizerGuard { }
/**
* An expression whose value is interpreted as HTML
* and may be inserted into the DOM through a library.
@@ -302,6 +387,42 @@ module DomBasedXss {
private class MetacharEscapeSanitizer extends Sanitizer, Shared::MetacharEscapeSanitizer { }
private class UriEncodingSanitizer extends Sanitizer, Shared::UriEncodingSanitizer { }
private class IsEscapedInSwitchSanitizer extends Sanitizer, Shared::IsEscapedInSwitchSanitizer { }
private class QuoteGuard extends SanitizerGuard, Shared::QuoteGuard { }
/**
* Holds if there exists two dataflow edges to `succ`, where one edges is sanitized, and the other edge starts with `pred`.
*/
predicate isOptionallySanitizedEdge(DataFlow::Node pred, DataFlow::Node succ) {
exists(HtmlSanitizerCall sanitizer |
// sanitized = sanitize ? sanitizer(source) : source;
exists(ConditionalExpr branch, Variable var, VarAccess access |
branch = succ.asExpr() and access = var.getAnAccess()
|
branch.getABranch() = access and
pred.getEnclosingExpr() = access and
sanitizer = branch.getABranch().flow() and
sanitizer.getAnArgument().getEnclosingExpr() = var.getAnAccess()
)
or
// sanitized = source; if (sanitize) {sanitized = sanitizer(source)};
exists(SsaPhiNode phi, SsaExplicitDefinition a, SsaDefinition b |
a = phi.getAnInput().getDefinition() and
b = phi.getAnInput().getDefinition() and
count(phi.getAnInput()) = 2 and
not a = b and
sanitizer = DataFlow::valueNode(a.getDef().getSource()) and
sanitizer.getAnArgument().asExpr().(VarAccess).getVariable() = b.getSourceVariable()
|
pred = DataFlow::ssaDefinitionNode(b) and
succ = DataFlow::ssaDefinitionNode(phi)
)
)
}
private class ContainsHTMLGuard extends SanitizerGuard, Shared::ContainsHTMLGuard { }
}
/** Provides classes and predicates for the reflected XSS query. */
@@ -315,6 +436,9 @@ module ReflectedXss {
/** A sanitizer for reflected XSS vulnerabilities. */
abstract class Sanitizer extends Shared::Sanitizer { }
/** A sanitizer guard for reflected XSS vulnerabilities. */
abstract class SanitizerGuard extends Shared::SanitizerGuard { }
/**
* An expression that is sent as part of an HTTP response, considered as an XSS sink.
*
@@ -401,6 +525,12 @@ module ReflectedXss {
private class MetacharEscapeSanitizer extends Sanitizer, Shared::MetacharEscapeSanitizer { }
private class UriEncodingSanitizer extends Sanitizer, Shared::UriEncodingSanitizer { }
private class IsEscapedInSwitchSanitizer extends Sanitizer, Shared::IsEscapedInSwitchSanitizer { }
private class QuoteGuard extends SanitizerGuard, Shared::QuoteGuard { }
private class ContainsHTMLGuard extends SanitizerGuard, Shared::ContainsHTMLGuard { }
}
/** Provides classes and predicates for the stored XSS query. */
@@ -414,6 +544,9 @@ module StoredXss {
/** A sanitizer for stored XSS vulnerabilities. */
abstract class Sanitizer extends Shared::Sanitizer { }
/** A sanitizer guard for stored XSS vulnerabilities. */
abstract class SanitizerGuard extends Shared::SanitizerGuard { }
/** An arbitrary XSS sink, considered as a flow sink for stored XSS. */
private class AnySink extends Sink {
AnySink() { this instanceof Shared::Sink }
@@ -429,6 +562,12 @@ module StoredXss {
private class MetacharEscapeSanitizer extends Sanitizer, Shared::MetacharEscapeSanitizer { }
private class UriEncodingSanitizer extends Sanitizer, Shared::UriEncodingSanitizer { }
private class IsEscapedInSwitchSanitizer extends Sanitizer, Shared::IsEscapedInSwitchSanitizer { }
private class QuoteGuard extends SanitizerGuard, Shared::QuoteGuard { }
private class ContainsHTMLGuard extends SanitizerGuard, Shared::ContainsHTMLGuard { }
}
/** Provides classes and predicates for the XSS through DOM query. */

View File

@@ -31,7 +31,12 @@ module XssThroughDom {
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof TypeTestGuard or
guard instanceof UnsafeJQuery::PropertyPresenceSanitizer
guard instanceof UnsafeJQuery::PropertyPresenceSanitizer or
guard instanceof DomBasedXss::SanitizerGuard
}
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
}
}

View File

@@ -13,17 +13,31 @@ module ZipSlip {
import ZipSlipCustomizations::ZipSlip
/** A taint tracking configuration for unsafe archive extraction. */
class Configuration extends TaintTracking::Configuration {
class Configuration extends DataFlow::Configuration {
Configuration() { this = "ZipSlip" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
label = source.(Source).getAFlowLabel()
}
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
label = sink.(Sink).getAFlowLabel()
}
override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof Sanitizer }
override predicate isBarrier(DataFlow::Node node) {
super.isBarrier(node) or
node instanceof TaintedPath::Sanitizer
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode nd) {
nd instanceof SanitizerGuard
override predicate isBarrierGuard(DataFlow::BarrierGuardNode guard) {
guard instanceof TaintedPath::BarrierGuardNode
}
override predicate isAdditionalFlowStep(
DataFlow::Node src, DataFlow::Node dst, DataFlow::FlowLabel srclabel,
DataFlow::FlowLabel dstlabel
) {
TaintedPath::isAdditionalTaintedPathFlowStep(src, dst, srclabel, dstlabel)
}
}
}

View File

@@ -7,25 +7,23 @@
import javascript
module ZipSlip {
import TaintedPathCustomizations::TaintedPath as TaintedPath
/**
* A data flow source for unsafe archive extraction.
*/
abstract class Source extends DataFlow::Node { }
abstract class Source extends DataFlow::Node {
/** Gets a flow label denoting the type of value for which this is a source. */
TaintedPath::Label::PosixPath getAFlowLabel() { result.isRelative() }
}
/**
* A data flow sink for unsafe archive extraction.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for unsafe archive extraction.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A sanitizer guard for unsafe archive extraction.
*/
abstract class SanitizerGuard extends TaintTracking::SanitizerGuardNode, DataFlow::ValueNode { }
abstract class Sink extends DataFlow::Node {
/** Gets a flow label denoting the type of value for which this is a sink. */
TaintedPath::Label::PosixPath getAFlowLabel() { any() }
}
/**
* Gets a node that can be a parsed archive.
@@ -122,42 +120,4 @@ module ZipSlip {
class FileSystemWriteSink extends Sink {
FileSystemWriteSink() { exists(FileSystemWriteAccess fsw | fsw.getAPathArgument() = this) }
}
/** An expression that sanitizes by calling path.basename */
class BasenameSanitizer extends Sanitizer {
BasenameSanitizer() { this = DataFlow::moduleImport("path").getAMemberCall("basename") }
}
/**
* An expression that forces the output path to be in the current working folder.
* Recognizes the pattern: `path.join(cwd, path.join('/', orgPath))`.
*/
class PathSanitizer extends Sanitizer, DataFlow::CallNode {
PathSanitizer() {
this = NodeJSLib::Path::moduleMember("join").getACall() and
exists(DataFlow::CallNode inner | inner = getArgument(1) |
inner = NodeJSLib::Path::moduleMember("join").getACall() and
inner.getArgument(0).mayHaveStringValue("/")
)
}
}
/**
* Gets a string which is sufficient to exclude to make
* a filepath definitely not refer to parent directories.
*/
private string getAParentDirName() { result = ".." or result = "../" }
/** A check that a path string does not include '..' */
class NoParentDirSanitizerGuard extends SanitizerGuard {
StringOps::Includes incl;
NoParentDirSanitizerGuard() { this = incl }
override predicate sanitizes(boolean outcome, Expr e) {
incl.getPolarity().booleanNot() = outcome and
incl.getBaseString().asExpr() = e and
incl.getSubstring().mayHaveStringValue(getAParentDirName())
}
}
}