Merge remote-tracking branch 'upstream/master' into deferredModel

This commit is contained in:
Erik Krogh Kristensen
2019-11-05 15:08:47 +01:00
911 changed files with 38307 additions and 11309 deletions

View File

@@ -1,37 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Builtin functions and objects defined in the JavaScript standard library can be shadowed or redefined in user code.
This is confusing and makes code hard to understand, so it should be avoided.
</p>
</overview>
<recommendation>
<p>
Refactor the code to avoid shadowing or redefinition. For example, if a local variable has the same name as a standard
library builtin, it should be renamed.
</p>
</recommendation>
<example>
<p>
In the following example, the user-defined function <code>eval</code> shadows the builtin function <code>eval</code>
defined in the standard library. It could be renamed <code>evaluate</code> to avoid confusion.
</p>
<sample src="examples/BuiltinRedefined.js" />
</example>
<references>
<li>Ecma International, <i>ECMAScript Language Definition</i>, 5.1 Edition, Section 15. ECMA, 2011.</li>
</references>
</qhelp>

View File

@@ -1,50 +0,0 @@
/**
* @name Builtin redefined
* @description Standard library functions can be redefined, but this should be avoided
* since it makes code hard to read and maintain.
* @kind problem
* @problem.severity recommendation
* @id js/builtin-redefinition
* @tags maintainability
* @precision medium
* @deprecated This query is prone to false positives. Deprecated since 1.17.
*/
import javascript
import Definitions
/**
* Holds if `id` is a redefinition of a standard library function that is considered
* acceptable since it merely introduces a local alias to the standard function of
* the same name.
*/
predicate acceptableRedefinition(Identifier id) {
// function(x, y, undefined) { ... }(23, 42)
id.getName() = "undefined" and
exists(ImmediatelyInvokedFunctionExpr iife |
id = iife.getParameter(iife.getInvocation().getNumArgument())
)
or
// Date = global.Date
exists(AssignExpr assgn |
id = assgn.getTarget() and
id.getName() = assgn.getRhs().getUnderlyingValue().(PropAccess).getPropertyName()
)
or
// var Date = global.Date
exists(VariableDeclarator decl |
id = decl.getBindingPattern() and
id.getName() = decl.getInit().getUnderlyingValue().(PropAccess).getPropertyName()
)
}
from DefiningIdentifier id, string name
where
not id.inExternsFile() and
name = id.getName() and
name
.regexpMatch("Object|Function|Array|String|Boolean|Number|Math|Date|RegExp|Error|" +
"NaN|Infinity|undefined|eval|parseInt|parseFloat|isNaN|isFinite|" +
"decodeURI|decodeURIComponent|encodeURI|encodeURIComponent") and
not acceptableRedefinition(id)
select id, "Redefinition of " + name + "."

View File

@@ -1,46 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Defining a method by assigning a closure to a property of the receiver object in the constructor
is inefficient, since a new closure is created for every instance. This wastes heap space and may
interfere with JIT compilation.
</p>
</overview>
<recommendation>
<p>
Assign the function to a property of the prototype object instead. That way, all instances share
the same closure.
</p>
</recommendation>
<example>
<p>
In the following example, constructor <code>Point</code> defines method <code>move</code> by creating
a new closure and storing it in the <code>move</code> property of each new instance. Consequently,
<code>p.move</code> and <code>q.move</code> are different methods.
</p>
<sample src="examples/InefficientMethodDefinition.js" />
<p>
It is better to instead define <code>move</code> on the prototype object <code>Point.prototype</code>
like this:
</p>
<sample src="examples/InefficientMethodDefinitionGood.js" />
</example>
<references>
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">Inheritance and the prototype chain</a>.</li>
</references>
</qhelp>

View File

@@ -1,41 +0,0 @@
/**
* @name Inefficient method definition
* @description Defining methods in the constructor (as opposed to adding them to the
* prototype object) is inefficient.
* @kind problem
* @problem.severity recommendation
* @id js/method-definition-in-constructor
* @tags efficiency
* maintainability
* @precision medium
* @deprecated This query is prone to false positives. Deprecated since 1.17.
*/
import javascript
import semmle.javascript.RestrictedLocations
/**
* Holds if `stmt` is of the form `this.<name> = <method>;`.
*/
predicate methodDefinition(ExprStmt stmt, string name, Function method) {
exists(AssignExpr assgn, PropAccess pacc |
assgn = stmt.getExpr() and
pacc = assgn.getLhs() and
pacc.getBase() instanceof ThisExpr and
name = pacc.getPropertyName() and
method = assgn.getRhs()
)
}
from Function ctor, ExprStmt defn, string name, Function method
where
not ctor instanceof ImmediatelyInvokedFunctionExpr and
defn = ctor.getABodyStmt() and
methodDefinition(defn, name, method) and
// if the method captures a local variable of the constructor, it cannot
// easily be moved to the constructor object
not exists(Variable v | v.getScope() = ctor.getScope() |
v.getAnAccess().getContainer().getEnclosingContainer*() = method
)
select defn.(FirstLineOf),
name + " should be added to the prototype object rather than to each instance."

View File

@@ -0,0 +1,67 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
The TypeScript compiler has to choose which specific overload is called
when a method with multiple overloads is called.
The compiler will always choose the textually first overload that does
not give rise to any type errors with the arguments provided at the
function call.
</p>
<p>
This behavior can be unintuitive for programmers unfamiliar with the
type system in TypeScript, and can in some instances lead to situations
where a programmer writes an overloaded method where only the first
overload can ever be used.
</p>
</overview>
<recommendation>
<p>
Either reorder the method overloads if an overload with more type
parameters is placed before a similar overload with fewer parameters.
Alternatively, collapse multiple overloads with identical parameter types by
creating a single overload that returns a union of the return types
from the multiple overloads.
</p>
</recommendation>
<example>
<p>
In the example below, a programmer has tried to express that a method
can return multiple possible values by creating multiple overloads
with identical parameter types. However, only the first overload
will ever be selected by the TypeScript compiler.
</p>
<sample src="examples/UnreachableMethodOverloads.ts" />
<p>
The error can be fixed by merging the overloads into a single method
signature that returns a union of the previous return types.
</p>
<sample src="examples/UnreachableMethodOverloadsGood.ts" />
<p>
In the example below, an interface <code>Foo</code> declares a method
<code>create()</code> with two overloads. The only difference between
the two overloads is the type parameter <code>T</code> in the first
overload. The TypeScript compiler will always use the first overload
when <code>create()</code> is called, as a default type will be used
for the type parameter <code>T</code> if none is provided.
This default type is <code>unknown</code> in TypeScript 3.5+, and
<code>{}</code> in earlier versions.
</p>
<sample src="examples/UnreachableMethodOverloadsTypeParameters.ts" />
<p>
In this example, the error has been fixed by switching the order of the two
overloads. In this fixed version, if the <code>create()</code> method
is called with an explicit type argument the second overload will be
used, as the first overload would give rise to a type error.
</p>
<sample src="examples/UnreachableMethodOverloadsTypeParametersGood.ts" />
</example>
<references>
<li>TypeScript specification: <a href="https://github.com/microsoft/TypeScript/blob/7be7cba050799bc11c9411babd31f44c9ec087f0/doc/spec.md#4.15.1">Overload Resolution</a></li>
</references>
</qhelp>

View File

@@ -0,0 +1,140 @@
/**
* @name Unreachable method overloads
* @description Having multiple overloads with the same parameter types in TypeScript
* makes all overloads except the first one unreachable, as the compiler
* always resolves calls to the textually first matching overload.
* @kind problem
* @problem.severity warning
* @id js/unreachable-method-overloads
* @precision high
* @tags correctness
* typescript
*/
import javascript
/**
* Gets the `i`th parameter from the method signature.
*/
SimpleParameter getParameter(MethodSignature sig, int i) { result = sig.getBody().getParameter(i) }
/**
* Gets a string-representation of the type-annotation from the `i`th parameter in the method signature.
*/
string getParameterTypeAnnotation(MethodSignature sig, int i) {
result = getParameter(sig, i).getTypeAnnotation().toString()
}
/**
* Gets the other overloads for an overloaded method signature.
*/
MethodSignature getOtherMatchingSignatures(MethodSignature sig) {
signaturesMatch(result, sig) and
result != sig
}
/**
* Gets the kind of the member-declaration. Either "static" or "instance".
*/
string getKind(MemberDeclaration m) {
if m.isStatic() then result = "static" else result = "instance"
}
/**
* A call-signature that originates from a MethodSignature in the AST.
*/
private class MethodCallSig extends CallSignatureType {
string name;
MethodCallSig() {
exists(MethodSignature sig |
this = sig.getBody().getCallSignature() and
name = sig.getName()
)
}
/**
* Gets the name of any member that has this signature.
*/
string getName() {
result = name
}
}
/**
* Holds if the two call signatures could be overloads of each other and have the same parameter types.
*/
predicate matchingCallSignature(MethodCallSig method, MethodCallSig other) {
method.getName() = other.getName() and
method.getNumOptionalParameter() = other.getNumOptionalParameter() and
method.getNumParameter() = other.getNumParameter() and
method.getNumRequiredParameter() = other.getNumRequiredParameter() and
// purposely not looking at number of type arguments.
method.getKind() = other.getKind() and
forall(int i | i in [0 .. -1 + method.getNumParameter()] |
method.getParameter(i) = other.getParameter(i) // This is sometimes imprecise, so it is still a good idea to compare type annotations.
) and
// shared type parameters are equal.
forall(int i | i in [0 .. -1 + min(int num | num = method.getNumTypeParameter() or num = other.getNumTypeParameter())] |
method.getTypeParameterBound(i) = other.getTypeParameterBound(i)
)
}
/**
* Gets which overload index the MethodSignature has among the overloads of the same name.
*/
int getOverloadIndex(MethodSignature sig) {
sig.getDeclaringType().getMethodOverload(sig.getName(), result) = sig
}
/**
* Holds if the two method signatures are overloads of each other and have the same parameter types.
*/
predicate signaturesMatch(MethodSignature method, MethodSignature other) {
// declared in the same interface/class.
method.getDeclaringType() = other.getDeclaringType() and
// same static modifier.
getKind(method) = getKind(other) and
// same name.
method.getName() = other.getName() and
// same number of parameters.
method.getBody().getNumParameter() = other.getBody().getNumParameter() and
// The types are compared in matchingCallSignature. This is sanity-check that the textual representation of the type-annotations are somewhat similar.
forall(int i | i in [0 .. -1 + method.getBody().getNumParameter()] |
getParameterTypeAnnotation(method, i) = getParameterTypeAnnotation(other, i)
) and
matchingCallSignature(method.getBody().getCallSignature(), other.getBody().getCallSignature())
}
from ClassOrInterface decl, string name, MethodSignature previous, MethodSignature unreachable
where
previous = decl.getMethod(name) and
unreachable = getOtherMatchingSignatures(previous) and
// If the method is part of inheritance between classes/interfaces, then there can sometimes be reasons for having this pattern.
not exists(decl.getASuperTypeDeclaration().getMethod(name)) and
not exists(ClassOrInterface sub |
decl = sub.getASuperTypeDeclaration() and
exists(sub.getMethod(name))
) and
// If a later method overload has more type parameters, then that overload can be selected by explicitly declaring the type arguments at the callsite.
// This comparison removes those cases.
unreachable.getBody().getNumTypeParameter() <= previous.getBody().getNumTypeParameter() and
// We always select the first of the overloaded methods.
not exists(MethodSignature later | later = getOtherMatchingSignatures(previous) |
getOverloadIndex(later) < getOverloadIndex(previous)
)
select unreachable,
"This overload of " + name + "() is unreachable, the $@ overload will always be selected.", previous, "previous"

View File

@@ -0,0 +1,5 @@
interface Foo {
getParsedThing(id: string): string[];
getParsedThing(id: string): number[];
getParsedThing(id: string): object[];
}

View File

@@ -0,0 +1,3 @@
interface Foo {
getParsedThing(id: string): object[] | number[] | string[];
}

View File

@@ -0,0 +1,4 @@
interface Foo {
create<T>(a: string): MyObject<T>;
create(a: string): MyObject<any>;
}

View File

@@ -0,0 +1,4 @@
interface Foo {
create(a: string): Array<any>;
create<T>(a: string): Array<T>;
}

View File

@@ -1,45 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Avoid using <code>x % 2 === 1</code> or <code>x % 2 &gt; 0</code> to check whether a number
<code>x</code> is odd, or <code>x % 2 !== 1</code> to check whether it is even.
Such code does not work for negative numbers: for example, <code>-5 % 2</code> equals
<code>-1</code>, not <code>1</code>.
</p>
</overview>
<recommendation>
<p>
Consider using <code>x % 2 !== 0</code> to check for odd parity and <code>x % 2 === 0</code>
to check for even parity.
</p>
</recommendation>
<example>
<p>
The following code snippet does not detect -9 as an odd number because <code>-9 % 2</code>
is <code>-1</code>, not <code>1</code>.</p>
<sample src="examples/BadParityCheck.js" />
<p>
The check should be rewritten as follows:
</p>
<sample src="examples/BadParityCheckGood.js" />
</example>
<references>
<li>J. Bloch and N. Gafter, <em>Java Puzzlers: Traps, Pitfalls, and Corner Cases</em>, Puzzle 1. Addison-Wesley, 2005.</li>
<li>Ecma International, <i>ECMAScript Language Definition</i>, 5.1 Edition, Section 11.5.3. ECMA, 2011.</li>
</references>
</qhelp>

View File

@@ -1,105 +0,0 @@
/**
* @name Bad parity check
* @description Ensure that parity checks take negative numbers into account.
* @kind problem
* @problem.severity recommendation
* @id js/incomplete-parity-check
* @tags reliability
* correctness
* external/cwe/cwe-480
* @precision low
* @deprecated This query is prone to false positives. Deprecated since 1.17.
*/
import javascript
/*
* The following predicates implement a simple analysis for identifying
* expressions that are guaranteed to only evaluate to non-negative numbers:
*
* - non-negative number literals
* - applications of (), ++, + to expressions known to be non-negative
* - references to local variables that are only assigned non-negative values,
* never decremented, and never subjected to any compound assignments except
* += where the rhs is known to be non-negative
*
* This is a greatest-fixpoint problem: if we have `x = 0`, `y = x`, `x = y`,
* we want to conclude that both `x` and `y` are non-negative. Hence we have
* to implement the analysis the other way around, as a conservative check
* for negativity.
*/
/**
* Holds if `e` is an expression that is relevant for the maybe-negative analysis.
*/
predicate relevant(Expr e) {
// base case: left operands of `%`
exists(ModExpr me | e = me.getLeftOperand())
or
// first inductive case: downward AST traversal
relevant(e.getParentExpr())
or
// second inductive case: following variable assignments
exists(Variable v | relevant(v.getAnAccess()) | e = v.getAnAssignedExpr())
}
/** Holds if `e` could evaluate to a negative number. */
predicate maybeNegative(Expr e) {
relevant(e) and
if exists(e.getIntValue())
then e.getIntValue() < 0
else
if e instanceof ParExpr
then maybeNegative(e.(ParExpr).getExpression())
else
if e instanceof IncExpr
then maybeNegative(e.(IncExpr).getOperand())
else
if e instanceof VarAccess
then maybeNegativeVar(e.(VarAccess).getVariable())
else
if e instanceof AddExpr
then maybeNegative(e.(AddExpr).getAnOperand())
else
// anything else is considered to possibly be negative
any()
}
/** Holds if `v` could be assigned a negative number. */
predicate maybeNegativeVar(Variable v) {
v.isGlobal()
or
v.isParameter()
or
// is v ever assigned a potentially negative value?
maybeNegative(v.getAnAssignedExpr())
or
// is v ever decremented?
exists(DecExpr dec | dec.getOperand().getUnderlyingReference() = v.getAnAccess())
or
// is v ever subject to a compound assignment other than +=, or to
// += with potentially negative rhs?
exists(CompoundAssignExpr assgn | assgn.getTarget() = v.getAnAccess() |
not assgn instanceof AssignAddExpr or
maybeNegative(assgn.getRhs())
)
}
from Comparison cmp, ModExpr me, int num, string parity
where
cmp.getAnOperand().stripParens() = me and
cmp.getAnOperand().getIntValue() = num and
me.getRightOperand().getIntValue() = 2 and
maybeNegative(me.getLeftOperand()) and
(
(cmp instanceof EqExpr or cmp instanceof StrictEqExpr) and
num = 1 and
parity = "oddness"
or
(cmp instanceof NEqExpr or cmp instanceof StrictNEqExpr) and
num = 1 and
parity = "evenness"
or
cmp instanceof GTExpr and num = 0 and parity = "oddness"
)
select cmp, "Test for " + parity + " does not take negative numbers into account."

View File

@@ -13,122 +13,10 @@
*/
import javascript
import DOMProperties
import semmle.javascript.frameworks.xUnit
import semmle.javascript.RestrictedLocations
import ExprHasNoEffect
import semmle.javascript.RestrictedLocations
/**
* Holds if `e` is of the form `x;` or `e.p;` and has a JSDoc comment containing a tag.
* In that case, it is probably meant as a declaration and shouldn't be flagged by this query.
*
* This will still flag cases where the JSDoc comment contains no tag at all (and hence carries
* no semantic information), and expression statements with an ordinary (non-JSDoc) comment
* attached to them.
*/
predicate isDeclaration(Expr e) {
(e instanceof VarAccess or e instanceof PropAccess) and
exists(e.getParent().(ExprStmt).getDocumentation().getATag())
}
/**
* Holds if there exists a getter for a property called `name` anywhere in the program.
*/
predicate isGetterProperty(string name) {
// there is a call of the form `Object.defineProperty(..., name, descriptor)` ...
exists(CallToObjectDefineProperty defProp | name = defProp.getPropertyName() |
// ... where `descriptor` defines a getter
defProp.hasPropertyAttributeWrite("get", _)
or
// ... where `descriptor` may define a getter
exists(DataFlow::SourceNode descriptor | descriptor.flowsTo(defProp.getPropertyDescriptor()) |
descriptor.isIncomplete(_)
or
// minimal escape analysis for the descriptor
exists(DataFlow::InvokeNode invk |
not invk = defProp and
descriptor.flowsTo(invk.getAnArgument())
)
)
)
or
// there is an object expression with a getter property `name`
exists(ObjectExpr obj | obj.getPropertyByName(name) instanceof PropertyGetter)
}
/**
* A property access that may invoke a getter.
*/
class GetterPropertyAccess extends PropAccess {
override predicate isImpure() { isGetterProperty(getPropertyName()) }
}
/**
* Holds if `c` is an indirect eval call of the form `(dummy, eval)(...)`, where
* `dummy` is some expression whose value is discarded, and which simply
* exists to prevent the call from being interpreted as a direct eval.
*/
predicate isIndirectEval(CallExpr c, Expr dummy) {
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
dummy = seq.getOperand(0) and
seq.getOperand(1).(GlobalVarAccess).getName() = "eval" and
seq.getNumOperands() = 2
)
}
/**
* Holds if `c` is a call of the form `(dummy, e[p])(...)`, where `dummy` is
* some expression whose value is discarded, and which simply exists
* to prevent the call from being interpreted as a method call.
*/
predicate isReceiverSuppressingCall(CallExpr c, Expr dummy, PropAccess callee) {
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
dummy = seq.getOperand(0) and
seq.getOperand(1) = callee and
seq.getNumOperands() = 2
)
}
/**
* Holds if evaluating `e` has no side effects (except potentially allocating
* and initializing a new object).
*
* For calls, we do not check whether their arguments have any side effects:
* even if they do, the call itself is useless and should be flagged by this
* query.
*/
predicate noSideEffects(Expr e) {
e.isPure()
or
// `new Error(...)`, `new SyntaxError(...)`, etc.
forex(Function f | f = e.flow().(DataFlow::NewNode).getACallee() |
f.(ExternalType).getASupertype*().getName() = "Error"
)
}
from Expr e
where
noSideEffects(e) and
inVoidContext(e) and
// disregard pure expressions wrapped in a void(...)
not e instanceof VoidExpr and
// filter out directives (unknown directives are handled by UnknownDirective.ql)
not exists(Directive d | e = d.getExpr()) and
// or about externs
not e.inExternsFile() and
// don't complain about declarations
not isDeclaration(e) and
// exclude DOM properties, which sometimes have magical auto-update properties
not isDOMProperty(e.(PropAccess).getPropertyName()) and
// exclude xUnit.js annotations
not e instanceof XUnitAnnotation and
// exclude common patterns that are most likely intentional
not isIndirectEval(_, e) and
not isReceiverSuppressingCall(_, e, _) and
// exclude anonymous function expressions as statements; these can only arise
// from a syntax error we already flag
not exists(FunctionExpr fe, ExprStmt es | fe = e |
fe = es.getExpr() and
not exists(fe.getName())
)
where hasNoEffect(e)
select e.(FirstLineOf), "This expression has no effect."

View File

@@ -3,6 +3,8 @@
*/
import javascript
import DOMProperties
import semmle.javascript.frameworks.xUnit
/**
* Holds if `e` appears in a syntactic context where its value is discarded.
@@ -37,3 +39,121 @@ predicate inVoidContext(Expr e) {
or
exists(LogicalBinaryExpr logical | e = logical.getRightOperand() and inVoidContext(logical))
}
/**
* Holds if `e` is of the form `x;` or `e.p;` and has a JSDoc comment containing a tag.
* In that case, it is probably meant as a declaration and shouldn't be flagged by this query.
*
* This will still flag cases where the JSDoc comment contains no tag at all (and hence carries
* no semantic information), and expression statements with an ordinary (non-JSDoc) comment
* attached to them.
*/
predicate isDeclaration(Expr e) {
(e instanceof VarAccess or e instanceof PropAccess) and
exists(e.getParent().(ExprStmt).getDocumentation().getATag())
}
/**
* Holds if there exists a getter for a property called `name` anywhere in the program.
*/
predicate isGetterProperty(string name) {
// there is a call of the form `Object.defineProperty(..., name, descriptor)` ...
exists(CallToObjectDefineProperty defProp | name = defProp.getPropertyName() |
// ... where `descriptor` defines a getter
defProp.hasPropertyAttributeWrite("get", _)
or
// ... where `descriptor` may define a getter
exists(DataFlow::SourceNode descriptor | descriptor.flowsTo(defProp.getPropertyDescriptor()) |
descriptor.isIncomplete(_)
or
// minimal escape analysis for the descriptor
exists(DataFlow::InvokeNode invk |
not invk = defProp and
descriptor.flowsTo(invk.getAnArgument())
)
)
)
or
// there is an object expression with a getter property `name`
exists(ObjectExpr obj | obj.getPropertyByName(name) instanceof PropertyGetter)
}
/**
* A property access that may invoke a getter.
*/
class GetterPropertyAccess extends PropAccess {
override predicate isImpure() { isGetterProperty(getPropertyName()) }
}
/**
* Holds if `c` is an indirect eval call of the form `(dummy, eval)(...)`, where
* `dummy` is some expression whose value is discarded, and which simply
* exists to prevent the call from being interpreted as a direct eval.
*/
predicate isIndirectEval(CallExpr c, Expr dummy) {
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
dummy = seq.getOperand(0) and
seq.getOperand(1).(GlobalVarAccess).getName() = "eval" and
seq.getNumOperands() = 2
)
}
/**
* Holds if `c` is a call of the form `(dummy, e[p])(...)`, where `dummy` is
* some expression whose value is discarded, and which simply exists
* to prevent the call from being interpreted as a method call.
*/
predicate isReceiverSuppressingCall(CallExpr c, Expr dummy, PropAccess callee) {
exists(SeqExpr seq | seq = c.getCallee().stripParens() |
dummy = seq.getOperand(0) and
seq.getOperand(1) = callee and
seq.getNumOperands() = 2
)
}
/**
* Holds if evaluating `e` has no side effects (except potentially allocating
* and initializing a new object).
*
* For calls, we do not check whether their arguments have any side effects:
* even if they do, the call itself is useless and should be flagged by this
* query.
*/
predicate noSideEffects(Expr e) {
e.isPure()
or
// `new Error(...)`, `new SyntaxError(...)`, etc.
forex(Function f | f = e.flow().(DataFlow::NewNode).getACallee() |
f.(ExternalType).getASupertype*().getName() = "Error"
)
}
/**
* Holds if the expression `e` should be reported as having no effect.
*/
predicate hasNoEffect(Expr e) {
noSideEffects(e) and
inVoidContext(e) and
// disregard pure expressions wrapped in a void(...)
not e instanceof VoidExpr and
// filter out directives (unknown directives are handled by UnknownDirective.ql)
not exists(Directive d | e = d.getExpr()) and
// or about externs
not e.inExternsFile() and
// don't complain about declarations
not isDeclaration(e) and
// exclude DOM properties, which sometimes have magical auto-update properties
not isDOMProperty(e.(PropAccess).getPropertyName()) and
// exclude xUnit.js annotations
not e instanceof XUnitAnnotation and
// exclude common patterns that are most likely intentional
not isIndirectEval(_, e) and
not isReceiverSuppressingCall(_, e, _) and
// exclude anonymous function expressions as statements; these can only arise
// from a syntax error we already flag
not exists(FunctionExpr fe, ExprStmt es | fe = e |
fe = es.getExpr() and
not exists(fe.getName())
)
}

View File

@@ -1,56 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
In JavaScript, properties of objects do not have to be declared and can be dynamically added
and removed at runtime. Thus, if a property name is misspelled, this is not detected by the
compiler, and may lead to an error at runtime. The same problem occurs with misspelled
global variables.
</p>
<p>
This rule flags property names and global variables that are mentioned only once, but where
a different capitalization of the same name is used in multiple other places, suggesting a typo.
</p>
</overview>
<recommendation>
<p>
Check whether the name has been misspelled. If the name is correct, consider using
a <a href="http://www.jslint.com/help.html#properties">JSLint-style</a>
<code>/*property ...*/</code> directive to document the existence of this property,
or provide an externs file declaring the property.
</p>
</recommendation>
<example>
<p>
The following code snippet contains two uses of the <code>log</code> method, but only
one use of the <code>Log</code> method. This suggests that <code>Log</code> may be a typo
for <code>log</code>.
</p>
<sample src="examples/HapaxLegomenon.js" />
<p>
If the use of <code>Log</code> is, in fact, a typo, it should be corrected. Otherwise, a
<code>properties</code> directive can be introduced to document the fact that both
<code>log</code> and <code>Log</code> properties exist:
</p>
<sample src="examples/HapaxLegomenonGood.js" />
</example>
<references>
<li>JSLint: <a href="http://www.jslint.com/help.html#properties">Property</a>.</li>
<li>Google Closure Tools: <a href="https://developers.google.com/closure/compiler/docs/api-tutorial3?csw=1#externs">Declaring externs</a>.</li>
</references>
</qhelp>

View File

@@ -1,91 +0,0 @@
/**
* @name Potentially misspelled property or variable name
* @description A property or variable is only mentioned once, but there is one with the same name
* in different capitalization that is mentioned more than once, suggesting that this
* may be a typo.
* @kind problem
* @problem.severity warning
* @id js/wrong-capitalization
* @tags reliability
* @precision low
* @deprecated This query is prone to false positives. Deprecated since 1.17.
*/
import javascript
/** Gets the number of identifiers and string literals that refer to `name`. */
int countOccurrences(string name) {
(
exists(PropAccess pacc | name = pacc.getPropertyName()) or
exists(VarAccess acc | name = acc.getName())
) and
result = strictcount(Expr id |
id.(Identifier).getName() = name
or
// count string literals as well to capture meta-programming
id.getStringValue() = name
)
}
/**
* An access to an undeclared variable or property that is only referenced
* once in the entire program.
*/
abstract class Hapax extends @expr {
/** Gets the name of the accessed variable or property. */
abstract string getName();
/** Gets a textual representation of this element. */
string toString() { result = this.(Expr).toString() }
}
/**
* An access to a property that is covered neither by a JSLint property declaration
* nor by an externs declaration, and that is only mentioned once in the entire program.
*/
class UndeclaredPropertyAccess extends Hapax, @dotexpr {
UndeclaredPropertyAccess() {
exists(string name | name = this.(DotExpr).getPropertyName() |
countOccurrences(name) = 1 and
not exists(JSLintProperties jslpd | jslpd.appliesTo(this) and jslpd.getAProperty() = name) and
not exists(ExternalMemberDecl emd | emd.getProperty() = this)
)
}
override string getName() { result = this.(DotExpr).getPropertyName() }
}
/**
* An access to a global variable that is neither declared nor covered by a linter
* directive, and that is only mentioned once in the entire program.
*/
class UndeclaredGlobal extends Hapax, @varaccess {
UndeclaredGlobal() {
exists(GlobalVariable gv, string name | this = gv.getAnAccess() and name = gv.getName() |
countOccurrences(name) = 1 and
not exists(Linting::GlobalDeclaration glob | glob.declaresGlobalForAccess(this)) and
not exists(gv.getADeclaration())
)
}
override string getName() { result = this.(VarAccess).getName() }
}
/**
* Gets the number of occurrences of `m`, which is the same as `hapax`
* except for capitalization, ensuring that it occurs at least twice.
*/
int candidateSpellingCount(Hapax hapax, string m) {
exists(string n | n = hapax.getName() |
m.toLowerCase() = n.toLowerCase() and
m != n and
result = countOccurrences(m) and
result > 1
)
}
from Hapax hapax, string n, string m
where
n = hapax.getName() and
candidateSpellingCount(hapax, m) = max(candidateSpellingCount(hapax, _))
select hapax.(Expr), "'" + n + "' is mentioned only once; it may be a typo for '" + m + "'."

View File

@@ -22,14 +22,51 @@ Expr leftChild(Expr e) {
result = e.(AddExpr).getLeftOperand()
}
class LiteralOrTemplate extends Expr {
LiteralOrTemplate() {
this instanceof TemplateLiteral or
this instanceof Literal
predicate isInConcat(Expr e) {
exists(ParExpr par | isInConcat(par) and par.getExpression() = e)
or
exists(AddExpr a | a.getAnOperand() = e)
}
class ConcatenationLiteral extends Expr {
ConcatenationLiteral() {
(
this instanceof TemplateLiteral
or
this instanceof Literal
)
and isInConcat(this)
}
}
from AddExpr e, LiteralOrTemplate l, LiteralOrTemplate r, string word
Expr getConcatChild(Expr e) {
result = rightChild(e) or
result = leftChild(e)
}
Expr getConcatParent(Expr e) {
e = getConcatChild(result)
}
predicate isWordLike(ConcatenationLiteral lit) {
lit.getStringValue().regexpMatch("(?i).*[a-z]{3,}.*")
}
class ConcatRoot extends AddExpr {
ConcatRoot() {
not isInConcat(this)
}
}
ConcatRoot getAddRoot(AddExpr e) {
result = getConcatParent*(e)
}
predicate hasWordLikeFragment(AddExpr e) {
isWordLike(getConcatChild*(getAddRoot(e)))
}
from AddExpr e, ConcatenationLiteral l, ConcatenationLiteral r, string word
where
// l and r are appended together
l = rightChild*(e.getLeftOperand()) and
@@ -41,5 +78,8 @@ where
// needed, and intra-identifier punctuation in, for example, a qualified name.
word = l.getStringValue().regexpCapture(".* (([-A-Za-z/'\\.:,]*[a-zA-Z]|[0-9]+)[\\.:,!?']*)", 1) and
r.getStringValue().regexpMatch("[a-zA-Z].*") and
not word.regexpMatch(".*[,\\.:].*[a-zA-Z].*[^a-zA-Z]")
not word.regexpMatch(".*[,\\.:].*[a-zA-Z].*[^a-zA-Z]") and
// There must be a constant-string in the concatenation that looks like a word.
hasWordLikeFragment(e)
select l, "This string appears to be missing a space after '" + word + "'."

View File

@@ -13,6 +13,8 @@ import javascript
from Directive d
where
not d instanceof KnownDirective and
// ignore ":" pseudo-directive sometimes seen in dual-use shell/node.js scripts
not d.getExpr().getStringValue() = ":" and
// but exclude attribute top-levels: `<a href="javascript:'some-attribute-string'">`
not d.getParent() instanceof CodeInAttribute
select d, "Unknown directive: '" + truncate(d.getDirectiveText(), 20, " ... (truncated)") + "'."

View File

@@ -1,38 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Non-standard JSDoc tags are undesirable, since JSDoc-processing tools will either ignore
them or treat them as plain text.
</p>
</overview>
<recommendation>
<p>
Check whether the tag name is misspelled, or consult the JSDoc documentation to find out
what standard tags are available.
</p>
</recommendation>
<example>
<p>
In the following example, the constructor <code>Message</code> has a JSDoc comment describing
its parameters, but the second <code>@param</code> tag has been misspelled as
<code>@parma</code>.
</p>
<sample src="examples/UnknownTagType.js" />
</example>
<references>
<li>Use JSDoc: <a href="http://usejsdoc.org/index.html">Tag Dictionary</a>.</li>
</references>
</qhelp>

View File

@@ -1,139 +0,0 @@
/**
* @name Unknown JSDoc tag
* @description A JSDoc tag with a non-standard tag type will either be ignored or treated as plain
* text by JSDoc-processing tools.
* @kind problem
* @problem.severity recommendation
* @id js/jsdoc/unknown-tag-type
* @tags maintainability
* readability
* documentation
* @precision low
* @deprecated This query is prone to false positives. Deprecated since 1.17.
*/
import javascript
/** Holds if `tp` is a standard tag type. */
predicate knownTagType(string tp) {
tp = "abstract" or
tp = "access" or
tp = "alias" or
tp = "api" or
tp = "arg" or
tp = "argument" or
tp = "augments" or
tp = "author" or
tp = "borrows" or
tp = "bug" or
tp = "callback" or
tp = "category" or
tp = "class" or
tp = "classdesc" or
tp = "const" or
tp = "constant" or
tp = "constructor" or
tp = "constructs" or
tp = "copyright" or
tp = "default" or
tp = "defaultvalue" or
tp = "define" or
tp = "depend" or
tp = "depends" or
tp = "deprecated" or
tp = "desc" or
tp = "description" or
tp = "dict" or
tp = "emits" or
tp = "enum" or
tp = "event" or
tp = "example" or
tp = "exception" or
tp = "export" or
tp = "exports" or
tp = "expose" or
tp = "extends" or
tp = "external" or
tp = "externs" or
tp = "field" or
tp = "file" or
tp = "fileoverview" or
tp = "final" or
tp = "fires" or
tp = "flow" or
tp = "func" or
tp = "function" or
tp = "global" or
tp = "host" or
tp = "ignore" or
tp = "implements" or
tp = "implicitCast" or
tp = "inheritDoc" or
tp = "inner" or
tp = "interface" or
tp = "internal" or
tp = "instance" or
tp = "kind" or
tp = "lends" or
tp = "license" or
tp = "link" or
tp = "member" or
tp = "memberof" or
tp = "memberOf" or
tp = "method" or
tp = "mixes" or
tp = "mixin" or
tp = "modifies" or
tp = "module" or
tp = "modName" or
tp = "mods" or
tp = "name" or
tp = "namespace" or
tp = "ngInject" or
tp = "noalias" or
tp = "nocompile" or
tp = "nosideeffects" or
tp = "note" or
tp = "override" or
tp = "overview" or
tp = "owner" or
tp = "package" or
tp = "param" or
tp = "preserve" or
tp = "preserveTry" or
tp = "private" or
tp = "prop" or
tp = "property" or
tp = "protected" or
tp = "providesModule" or
tp = "public" or
tp = "readonly" or
tp = "requires" or
tp = "returns" or
tp = "return" or
tp = "see" or
tp = "since" or
tp = "static" or
tp = "struct" or
tp = "summary" or
tp = "supported" or
tp = "suppress" or
tp = "template" or
tp = "this" or
tp = "throws" or
tp = "todo" or
tp = "tutorial" or
tp = "type" or
tp = "typedef" or
tp = "var" or
tp = "variation" or
tp = "version" or
tp = "virtual" or
tp = "visibility" or
tp = "wizaction" or
tp = "wizmodule"
}
from JSDocTag tag
where not knownTagType(tag.getTitle())
select tag, "Unknown tag type '" + tag.getTitle() + "'."

View File

@@ -1,26 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
JSLint directives must not start with a space. For example, <code>/* global window*/</code>
is not a valid directive, and will not be recognized by JSLint.
</p>
</overview>
<recommendation>
<p>
Remove the space: <code>/*global window*/</code>.
</p>
</recommendation>
<references>
<li>JSLint: <a href="http://www.jslint.com/help.html">JSLint Help</a>.</li>
</references>
</qhelp>

View File

@@ -1,21 +0,0 @@
/**
* @name Invalid JSLint directive
* @description A JSLint directive that has whitespace characters before the
* directive name is not recognized by JSLint.
* @kind problem
* @problem.severity recommendation
* @id js/jslint/invalid-directive
* @tags maintainability
* @precision medium
* @deprecated JSLint is rarely used any more. Deprecated since 1.17.
*/
import javascript
from SlashStarComment c
where
// use possessive quantifiers '*+' and '++' to avoid backtracking
c
.getText()
.regexpMatch("\\s+(global|properties|property|jslint)\\s(\\s*+[a-zA-Z$_][a-zA-Z0-9$_]*+(\\s*+:\\s*+\\w++)?\\s*+,?)++\\s*")
select c, "JSLint directives must not have whitespace characters before the directive name."

View File

@@ -1,49 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
JSLint directives must consist of a comma-separated list of flags, where each flag
can optionally be followed by a colon and a value. The value may either be a number
or a Boolean (that is, 'true' or 'false'). Directives must not contain other
characters such as '*', which some editors may automatically insert after every line
break when editing a block comment.
</p>
</overview>
<recommendation>
<p>
Insert commas where necessary and remove stray characters.
</p>
</recommendation>
<example>
<p>
For example, <code>/*jslint nomen:true vars:true*/</code> is not a well-formed
JSLint directive; it should be replaced by <code>/*jslint nomen:true, vars:true*/</code>.
</p>
<p>
This is another example of a malformed JSLint directive:
</p>
<sample src="examples/MalformedJSLintDirective.js" />
<p>
It should be fixed as follows:
</p>
<sample src="examples/MalformedJSLintDirectiveGood.js" />
</example>
<references>
<li>JSLint: <a href="http://www.jslint.com/help.html">JSLint Help</a>.</li>
</references>
</qhelp>

View File

@@ -1,26 +0,0 @@
/**
* @name Malformed JSLint directive
* @description A malformed JSLint directive will be rejected by JSLint, and may be either
* rejected or ignored by other tools.
* @kind problem
* @problem.severity recommendation
* @id js/jslint/malformed-directive
* @tags maintainability
* @precision medium
* @deprecated JSLint is rarely used any more. Deprecated since 1.17.
*/
import javascript
from JSLintDirective dir, string flag, string flags, string directive
where
// a flag, optionally followed by a colon and a value, where the value may be
// a Boolean or a number
flag = "[a-zA-Z$_][a-zA-Z0-9$_]*(\\s*:\\s*(true|false|\\d+))?" and
// a non-empty, comma-separated list of flags
flags = "(" + flag + "\\s*,\\s*)*" + flag and
// a word (which is the directive's name), followed by a possibly empty list of flags
// note that there may be trailing whitespace, but no leading whitespace
directive = "\\s*\\w+\\s+(" + flags + ")?\\s*" and
not dir.getText().regexpMatch(directive)
select dir, "Malformed JSLint directive."

View File

@@ -1,42 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
HTML comments are a technique for hiding JavaScript code from browsers that do not interpret <code>script</code>
tags. Since all popular browsers have supported <code>script</code> tags for many years, this precaution is
not needed any more.
</p>
</overview>
<recommendation>
<p>
Remove all HTML comments.
</p>
</recommendation>
<example>
<p>
The following code block uses HTML comments to hide the <code>script</code> block from ancient browsers.
</p>
<sample src="examples/HTMLComments.js" />
<p>
Since such browsers are no longer widely used, the comments should be removed:
</p>
<sample src="examples/HTMLCommentsGood.js" />
</example>
<references>
<li>JavaScript Toolbox: <a href="http://www.javascripttoolbox.com/bestpractices/#comments">JavaScript Best Practices</a>.</li>
</references>
</qhelp>

View File

@@ -1,18 +0,0 @@
/**
* @name Use of HTML comments
* @description HTML-style comments are not a standard ECMAScript feature and should be avoided.
* @kind problem
* @problem.severity recommendation
* @id js/html-comment
* @tags maintainability
* language-features
* external/cwe/cwe-758
* @precision low
* @deprecated HTML comments are recognized in the standard as an additional feature supported by
* web browsers. Deprecated since 1.17.
*/
import javascript
from HtmlLineComment c
select c, "Do not use HTML comments."

View File

@@ -61,5 +61,7 @@ where
forex(DataFlow::InvokeNode cs2, Function otherCallee |
cs2.getInvokeExpr() = cs.getInvokeExpr() and otherCallee = cs2.getACallee() |
illegalInvocation(cs, otherCallee, _, _)
)
) and
// require that all callees are known
not cs.isIncomplete()
select cs, "Illegal invocation of $@ " + how + ".", callee, calleeDesc

View File

@@ -1,40 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Multi-line string literals are not supported on all platforms, and thus should be avoided.
</p>
</overview>
<recommendation>
<p>
Replace multi-line string literals by multiple strings concatenated with the <code>+</code> operator.
</p>
</recommendation>
<example>
<p>
The following example contains a string literal spanning three lines:
</p>
<sample src="examples/MultilineStringLiteral.js" />
<p>
It should be rewritten like this:
</p>
<sample src="examples/MultilineStringLiteralGood.js" />
</example>
<references>
<li>Ecma International, <i>ECMAScript Language Definition</i>, 5.1 Edition, Section 7.8.4. ECMA, 2011.</li>
</references>
</qhelp>

View File

@@ -1,19 +0,0 @@
/**
* @name Multi-line string literal
* @description Multi-line string literals are non-standard and hard to read, and should be avoided.
* @kind problem
* @problem.severity recommendation
* @id js/multi-line-string
* @tags maintainability
* external/cwe/cwe-758
* @precision low
* @deprecated Multi-line string literals are now a standard language feature. Deprecated since 1.17.
*/
import javascript
from StringLiteral sl, Location l
where
l = sl.getLocation() and
l.getStartLine() != l.getEndLine()
select sl, "Avoid multi-line string literals."

View File

@@ -1,40 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Integer literals starting with the digit <code>0</code> may be interpreted as octal numbers by some platforms
but not others, and thus should be avoided. This does not make a difference for the literal <code>0</code>
itself.
</p>
</overview>
<recommendation>
<p>
If the literal was meant to be octal, convert it to a decimal or hexadecimal number. Otherwise, remove
the leading zero.
</p>
</recommendation>
<example>
<p>
The following example uses the literal <code>012</code>, which some platforms will interpret as an octal
encoding of the decimal number <code>10</code>, while others will interpret it as the decimal number
<code>12</code>. Depending on the desired interpretation, it should be replaced with either <code>10</code>
or <code>12</code>.
</p>
<sample src="examples/OctalLiteral.js" />
</example>
<references>
<li>Ecma International, <i>ECMAScript Language Definition</i>, 5.1 Edition, Annex B.1.1. ECMA, 2011.</li>
</references>
</qhelp>

View File

@@ -1,17 +0,0 @@
/**
* @name Octal literal
* @description Octal numeric literals are a platform-specific extension and should not be used.
* @kind problem
* @problem.severity recommendation
* @id js/octal-literal
* @tags portability
* external/cwe/cwe-758
* @precision low
* @deprecated This query is prone to false positives. Deprecated since 1.17.
*/
import javascript
from NumberLiteral nl
where nl.getRawValue().regexpMatch("0\\d+")
select nl, "Do not use octal literals."

View File

@@ -1,37 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
The ECMAScript standard defines a list of future keywords that should not be used as identifiers.
While they may be accepted by current implementations, they may no longer be supported in the future,
so it is best not to rely on them.
</p>
</overview>
<recommendation>
<p>
Rename the identifier in question.
</p>
</recommendation>
<example>
<p>
In the following code snippet, <code>package</code> is used as a variable name. Since <code>package</code>
is a future reserved word, the variable should be renamed, for instance to <code>pkg</code>.
</p>
<sample src="examples/ReservedWords.js" />
</example>
<references>
<li>Ecma International, <i>ECMAScript Language Definition</i>, 5.1 Edition, Section 7.6.1.2. ECMA, 2011.</li>
</references>
</qhelp>

View File

@@ -1,24 +0,0 @@
/**
* @name Reserved word used as variable name
* @description Future reserved words should not be used as variable names.
* @kind problem
* @problem.severity recommendation
* @id js/use-of-reserved-word
* @tags maintainability
* language-features
* @precision very-high
* @deprecated This is no longer a problem with modern browsers. Deprecated since 1.17.
*/
import javascript
from Identifier id
where
id
.getName()
.regexpMatch("class|const|enum|export|extends|import|super|implements|interface|let|package|private|protected|public|static|yield") and
not exists(DotExpr de | id = de.getProperty()) and
not exists(Property prop | id = prop.getNameExpr()) and
// exclude JSX attribute names
not exists(JSXElement e | id = e.getAnAttribute().getNameExpr())
select id, "Identifier name is a reserved word."

View File

@@ -1,37 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
The ECMAScript standard allows trailing commas in array and object literals which are ignored. However,
older versions of Internet Explorer do not recognize this syntax. Moreover, it can lead to confusion
when used in array literals, since spurious commas other than the last one are not ignored but give rise
to additional undefined array elements. For these reasons, trailing commas should always be avoided.
</p>
</overview>
<recommendation>
<p>
Remove the trailing comma.
</p>
</recommendation>
<example>
<p>
The following code snippet shows an object literal with a trailing comma, which should be removed.
</p>
<sample src="examples/TrailingComma.js" />
</example>
<references>
<li>Ecma International, <i>ECMAScript Language Definition</i>, 5.1 Edition, Sections 11.1.4 and 11.1.5. ECMA, 2011.</li>
</references>
</qhelp>

View File

@@ -1,37 +0,0 @@
/**
* @name Trailing comma in array or object expressions
* @description Trailing commas in array and object expressions are interpreted differently
* by different browsers and should be avoided.
* @kind problem
* @problem.severity recommendation
* @id js/trailing-comma-in-array-or-object
* @tags portability
* external/cwe/cwe-758
* @precision low
* @deprecated This is no longer a problem with modern browsers. Deprecated since 1.17.
*/
import javascript
/** An array or object expression. */
class ArrayOrObjectExpr extends Expr {
ArrayOrObjectExpr() {
this instanceof ArrayExpr or
this instanceof ObjectExpr
}
/** Holds if this array or object expression has a trailing comma. */
predicate hasTrailingComma() {
this.(ArrayExpr).hasTrailingComma() or
this.(ObjectExpr).hasTrailingComma()
}
/** Gets a short description of this expression. */
string getShortName() {
if this instanceof ArrayExpr then result = "array expression" else result = "object expression"
}
}
from ArrayOrObjectExpr e
where e.hasTrailingComma()
select e.getLastToken().getPreviousToken(), "Trailing comma in " + e.getShortName() + "."

View File

@@ -11,6 +11,7 @@
*/
import javascript
import semmle.javascript.CharacterEscapes
/**
* Holds if `pattern` is a regular expression pattern for URLs with a host matched by `hostPart`,
@@ -40,7 +41,9 @@ where
)
) and
// ignore patterns with capture groups after the TLD
not pattern.regexpMatch("(?i).*[.](" + RegExpPatterns::commonTLD() + ").*[(][?]:.*[)].*")
not pattern.regexpMatch("(?i).*[.](" + RegExpPatterns::commonTLD() + ").*[(][?]:.*[)].*") and
// avoid double reporting
not CharacterEscapes::hasALikelyRegExpPatternMistake(re)
select re,
"This " + kind + " has an unescaped '.' before '" + hostPart +
"', so it might match more hosts than expected.", aux, "here"

View File

@@ -0,0 +1,86 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
When a character in a string literal or regular expression
literal is preceded by a backslash, it is interpreted as part of an
escape sequence. For example, the escape sequence <code>\n</code> in a
string literal corresponds to a single <code>newline</code> character,
and not the <code>\</code> and <code>n</code> characters.
However, not all characters change meaning when used in an
escape sequence. In this case, the backslash just makes the character
appear to mean something else, and the backslash actually has no
effect. For example, the escape sequence <code>\k</code> in a string
literal just means <code>k</code>.
Such superfluous escape sequences are usually benign, and
do not change the behavior of the program.
</p>
<p>
The set of characters that change meaning when in escape
sequences is different for regular expression literals and string
literals.
This can be problematic when a regular expression literal
is turned into a regular expression that is built from one or more
string literals. The problem occurs when a regular expression escape
sequence loses its special meaning in a string literal.
</p>
</overview>
<recommendation>
<p>
Ensure that the right amount of backslashes is used when
escaping characters in strings, template literals and regular
expressions.
Pay special attention to the number of backslashes when
rewriting a regular expression as a string literal.
</p>
</recommendation>
<example>
<p>
The following example code checks that a string is
<code>"my-marker"</code>, possibly surrounded by white space:
</p>
<sample src="examples/UselessRegExpCharacterEscape_bad_1.js"/>
<p>
However, the check does not work properly for white space
as the two <code>\s</code> occurrences are semantically equivalent to
just <code>s</code>, meaning that the check will succeed for strings
like <code>"smy-markers"</code> instead of <code>" my-marker
"</code>.
Address these shortcomings by either using a regular
expression literal (<code>/(^\s*)my-marker(\s*$)/</code>), or by
adding extra backslashes
(<code>'(^\\s*)my-marker(\\s*$)'</code>).
</p>
</example>
<references>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping">Regular expression escape notation</a></li>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#Escape_notation">String escape notation</a></li>
</references>
</qhelp>

View File

@@ -0,0 +1,156 @@
/**
* @name Useless regular-expression character escape
* @description Prepending a backslash to an ordinary character in a string
* does not have any effect, and may make regular expressions constructed from this string
* behave unexpectedly.
* @kind problem
* @problem.severity error
* @precision high
* @id js/useless-regexp-character-escape
* @tags correctness
* security
* external/cwe/cwe-20
*/
import javascript
import semmle.javascript.CharacterEscapes::CharacterEscapes
newtype TRegExpPatternMistake =
/**
* A character escape mistake in regular expression string `src`
* for the character `char` at `index` in `rawStringNode`, explained
* by `mistake`.
*/
TIdentityEscapeInStringMistake(
RegExpPatternSource src, string char, string mistake, ASTNode rawStringNode, int index
) {
char = getALikelyRegExpPatternMistake(src, mistake, rawStringNode, index)
} or
/**
* A backslash-escaped 'b' at `index` of `rawStringNode` in the
* regular expression string `src`, indicating intent to use the
* word-boundary assertion '\b'.
*/
TBackspaceInStringMistake(RegExpPatternSource src, ASTNode rawStringNode, int index) {
exists(string raw, string cooked |
exists(StringLiteral lit | lit = rawStringNode |
rawStringNode = src.asExpr() and
raw = lit.getRawValue() and
cooked = lit.getStringValue()
)
or
exists(TemplateElement elem | elem = rawStringNode |
rawStringNode = src.asExpr().(TemplateLiteral).getAnElement() and
raw = elem.getRawValue() and
cooked = elem.getStringValue()
)
|
"b" = getAnEscapedCharacter(raw, index) and
// except if the string is exactly \b
cooked.length() != 1
)
}
/**
* A character escape mistake in a regular expression string.
*
* Implementation note: the main purpose of this class is to associate an
* exact character location with an alert message, in the name of
* user-friendly alerts. The implementation can be simplified
* significantly by only using the enclosing string location as the alert
* location.
*/
class RegExpPatternMistake extends TRegExpPatternMistake {
/**
* Holds if this element is at the specified location.
* The location spans column `startcolumn` of line `startline` to
* column `endcolumn` of line `endline` in file `filepath`.
* For more information, see
* [Locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
*/
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
exists(int srcStartcolumn, int srcEndcolumn, int index |
index = getIndex() and
getRawStringNode()
.getLocation()
.hasLocationInfo(filepath, startline, srcStartcolumn, endline, srcEndcolumn)
|
(
if startline = endline
then startcolumn = srcStartcolumn + index - 1 and endcolumn = srcStartcolumn + index
else (
startcolumn = srcStartcolumn and endcolumn = srcEndcolumn
)
)
)
}
/** Gets a textual representation of this element. */
string toString() { result = getMessage() }
abstract ASTNode getRawStringNode();
abstract RegExpPatternSource getSrc();
abstract int getIndex();
abstract string getMessage();
}
/**
* An identity-escaped character that indicates programmer intent to
* do something special in a regular expression.
*/
class IdentityEscapeInStringMistake extends RegExpPatternMistake, TIdentityEscapeInStringMistake {
RegExpPatternSource src;
string char;
string mistake;
int index;
ASTNode rawStringNode;
IdentityEscapeInStringMistake() {
this = TIdentityEscapeInStringMistake(src, char, mistake, rawStringNode, index)
}
override string getMessage() {
result = "'\\" + char + "' is equivalent to just '" + char + "', so the sequence " + mistake
}
override int getIndex() { result = index }
override RegExpPatternSource getSrc() { result = src }
override ASTNode getRawStringNode() { result = rawStringNode }
}
/**
* A backspace as '\b' in a regular expression string, indicating
* programmer intent to use the word-boundary assertion '\b'.
*/
class BackspaceInStringMistake extends RegExpPatternMistake, TBackspaceInStringMistake {
RegExpPatternSource src;
int index;
ASTNode rawStringNode;
BackspaceInStringMistake() { this = TBackspaceInStringMistake(src, rawStringNode, index) }
override string getMessage() { result = "'\\b' is a backspace, and not a word-boundary assertion" }
override int getIndex() { result = index }
override RegExpPatternSource getSrc() { result = src }
override ASTNode getRawStringNode() { result = rawStringNode }
}
from RegExpPatternMistake mistake
select mistake, "The escape sequence " + mistake.getMessage() + " when it is used in a $@.",
mistake.getSrc().getAParse(), "regular expression"

View File

@@ -0,0 +1,2 @@
let regex = new RegExp('(^\s*)my-marker(\s*$)'),
isMyMarkerText = regex.test(text);

View File

@@ -0,0 +1,103 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Dynamically constructing a shell command with values from the
local environment, such as file paths, may inadvertently
change the meaning of the shell command.
Such changes can occur when an environment value contains
characters that the shell interprets in a special way, for instance
quotes and spaces.
This can result in the shell command misbehaving, or even
allowing a malicious user to execute arbitrary commands on the system.
</p>
</overview>
<recommendation>
<p>
If possible, use hard-coded string literals to specify the
shell command to run, and provide the dynamic arguments to the shell
command separately to avoid interpretation by the shell.
</p>
<p>
Alternatively, if the shell command must be constructed
dynamically, then add code to ensure that special characters in
environment values do not alter the shell command unexpectedly.
</p>
</recommendation>
<example>
<p>
The following example shows a dynamically constructed shell
command that recursively removes a temporary directory that is located
next to the currently executing JavaScript file. Such utilities are
often found in custom build scripts.
</p>
<sample src="examples/shell-command-injection-from-environment.js" />
<p>
The shell command will, however, fail to work as intended if the
absolute path of the script's directory contains spaces. In that
case, the shell command will interpret the absolute path as multiple
paths, instead of a single path.
</p>
<p>
For instance, if the absolute path of
the temporary directory is <code>/home/username/important
project/temp</code>, then the shell command will recursively delete
<code>/home/username/important</code> and <code>project/temp</code>,
where the latter path gets resolved relative to the working directory
of the JavaScript process.
</p>
<p>
Even worse, although less likely, a malicious user could
provide the path <code>/home/username/; cat /etc/passwd #/important
project/temp</code> in order to execute the command <code>cat
/etc/passwd</code>.
</p>
<p>
To avoid such potentially catastrophic behaviors, provide the
directory as an argument that does not get interpreted by a
shell:
</p>
<sample src="examples/shell-command-injection-from-environment_fixed.js" />
</example>
<references>
<li>
OWASP:
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,29 @@
/**
* @name Shell command built from environment values
* @description Building a shell command string with values from the enclosing
* environment may cause subtle bugs or vulnerabilities.
* @kind path-problem
* @problem.severity warning
* @precision high
* @id js/shell-command-injection-from-environment
* @tags correctness
* security
* external/cwe/cwe-078
* external/cwe/cwe-088
*/
import javascript
import DataFlow::PathGraph
import semmle.javascript.security.dataflow.ShellCommandInjectionFromEnvironment::ShellCommandInjectionFromEnvironment
from
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node highlight,
Source sourceNode
where
sourceNode = source.getNode() and
cfg.hasFlowPath(source, sink) and
if cfg.isSinkWithHighlight(sink.getNode(), _)
then cfg.isSinkWithHighlight(sink.getNode(), highlight)
else highlight = sink.getNode()
select highlight, source, sink, "This shell command depends on an uncontrolled $@.", sourceNode,
sourceNode.getSourceType()

View File

@@ -0,0 +1,6 @@
var cp = require("child_process"),
path = require("path");
function cleanupTemp() {
let cmd = "rm -rf " + path.join(__dirname, "temp");
cp.execSync(cmd); // BAD
}

View File

@@ -0,0 +1,7 @@
var cp = require("child_process"),
path = require("path");
function cleanupTemp() {
let cmd = "rm",
args = ["-rf", path.join(__dirname, "temp")];
cp.execFileSync(cmd, args); // GOOD
}

View File

@@ -10,8 +10,8 @@ attacks such as cross-site scripting. One particular example of this is HTML ent
where HTML special characters are replaced by HTML character entities to prevent them from being
interpreted as HTML markup. For example, the less-than character is encoded as <code>&amp;lt;</code>
and the double-quote character as <code>&amp;quot;</code>.
Other examples include backslash-escaping for including untrusted data in string literals and
percent-encoding for URI components.
Other examples include backslash escaping or JSON encoding for including untrusted data in string
literals, and percent-encoding for URI components.
</p>
<p>
The reverse process of replacing escape sequences with the characters they represent is known as

View File

@@ -46,7 +46,12 @@ string getStringValue(RegExpLiteral rl) {
*/
DataFlow::Node getASimplePredecessor(DataFlow::Node nd) {
result = nd.getAPredecessor() and
not nd.(DataFlow::SsaDefinitionNode).getSsaVariable().getDefinition() instanceof SsaPhiNode
not exists(SsaDefinition ssa |
ssa = nd.(DataFlow::SsaDefinitionNode).getSsaVariable().getDefinition()
|
ssa instanceof SsaPhiNode or
ssa instanceof SsaVariableCapture
)
}
/**
@@ -54,38 +59,31 @@ DataFlow::Node getASimplePredecessor(DataFlow::Node nd) {
* into a form described by regular expression `regex`.
*/
predicate escapingScheme(string metachar, string regex) {
metachar = "&" and regex = "&.*;"
metachar = "&" and regex = "&.+;"
or
metachar = "%" and regex = "%.*"
metachar = "%" and regex = "%.+"
or
metachar = "\\" and regex = "\\\\.*"
metachar = "\\" and regex = "\\\\.+"
}
/**
* A call to `String.prototype.replace` that replaces all instances of a pattern.
* A method call that performs string replacement.
*/
class Replacement extends DataFlow::Node {
RegExpLiteral pattern;
Replacement() {
exists(DataFlow::MethodCallNode mcn | this = mcn |
mcn.getMethodName() = "replace" and
pattern.flow().(DataFlow::SourceNode).flowsTo(mcn.getArgument(0)) and
mcn.getNumArgument() = 2 and
pattern.isGlobal()
)
}
abstract class Replacement extends DataFlow::Node {
/**
* Holds if this replacement replaces the string `input` with `output`.
*/
predicate replaces(string input, string output) {
exists(DataFlow::MethodCallNode mcn |
mcn = this and
input = getStringValue(pattern) and
output = mcn.getArgument(1).getStringValue()
)
}
abstract predicate replaces(string input, string output);
/**
* Gets the input of this replacement.
*/
abstract DataFlow::Node getInput();
/**
* Gets the output of this replacement.
*/
abstract DataFlow::SourceNode getOutput();
/**
* Holds if this replacement escapes `char` using `metachar`.
@@ -118,9 +116,12 @@ class Replacement extends DataFlow::Node {
/**
* Gets the previous replacement in this chain of replacements.
*/
Replacement getPreviousReplacement() {
result = getASimplePredecessor*(this.(DataFlow::MethodCallNode).getReceiver())
}
Replacement getPreviousReplacement() { result.getOutput() = getASimplePredecessor*(getInput()) }
/**
* Gets the next replacement in this chain of replacements.
*/
Replacement getNextReplacement() { this = result.getPreviousReplacement() }
/**
* Gets an earlier replacement in this chain of replacements that
@@ -130,7 +131,9 @@ class Replacement extends DataFlow::Node {
exists(Replacement pred | pred = this.getPreviousReplacement() |
if pred.escapes(_, metachar)
then result = pred
else result = pred.getAnEarlierEscaping(metachar)
else (
not pred.unescapes(metachar, _) and result = pred.getAnEarlierEscaping(metachar)
)
)
}
@@ -142,11 +145,100 @@ class Replacement extends DataFlow::Node {
exists(Replacement succ | this = succ.getPreviousReplacement() |
if succ.unescapes(metachar, _)
then result = succ
else result = succ.getALaterUnescaping(metachar)
else (
not succ.escapes(_, metachar) and result = succ.getALaterUnescaping(metachar)
)
)
}
}
/**
* A call to `String.prototype.replace` that replaces all instances of a pattern.
*/
class GlobalStringReplacement extends Replacement, DataFlow::MethodCallNode {
RegExpLiteral pattern;
GlobalStringReplacement() {
this.getMethodName() = "replace" and
pattern.flow().(DataFlow::SourceNode).flowsTo(this.getArgument(0)) and
this.getNumArgument() = 2 and
pattern.isGlobal()
}
override predicate replaces(string input, string output) {
input = getStringValue(pattern) and
output = this.getArgument(1).getStringValue()
or
exists(DataFlow::FunctionNode replacer, DataFlow::PropRead pr, DataFlow::ObjectLiteralNode map |
replacer = getCallback(1) and
replacer.getParameter(0).flowsToExpr(pr.getPropertyNameExpr()) and
pr = map.getAPropertyRead() and
pr.flowsTo(replacer.getAReturn()) and
map.asExpr().(ObjectExpr).getPropertyByName(input).getInit().getStringValue() = output
)
}
override DataFlow::Node getInput() { result = this.getReceiver() }
override DataFlow::SourceNode getOutput() { result = this }
}
/**
* A call to `JSON.stringify`, viewed as a string replacement.
*/
class JsonStringifyReplacement extends Replacement, DataFlow::CallNode {
JsonStringifyReplacement() { this = DataFlow::globalVarRef("JSON").getAMemberCall("stringify") }
override predicate replaces(string input, string output) {
input = "\\" and output = "\\\\"
// the other replacements are not relevant for this query
}
override DataFlow::Node getInput() { result = this.getArgument(0) }
override DataFlow::SourceNode getOutput() { result = this }
}
/**
* A call to `JSON.parse`, viewed as a string replacement.
*/
class JsonParseReplacement extends Replacement {
JsonParserCall self;
JsonParseReplacement() { this = self }
override predicate replaces(string input, string output) {
input = "\\\\" and output = "\\"
// the other replacements are not relevant for this query
}
override DataFlow::Node getInput() { result = self.getInput() }
override DataFlow::SourceNode getOutput() { result = self.getOutput() }
}
/**
* A string replacement wrapped in a utility function.
*/
class WrappedReplacement extends Replacement, DataFlow::CallNode {
int i;
Replacement inner;
WrappedReplacement() {
exists(DataFlow::FunctionNode wrapped | wrapped.getFunction() = getACallee() |
wrapped.getParameter(i).flowsTo(inner.getPreviousReplacement*().getInput()) and
inner.getNextReplacement*().getOutput().flowsTo(wrapped.getAReturn())
)
}
override predicate replaces(string input, string output) { inner.replaces(input, output) }
override DataFlow::Node getInput() { result = getArgument(i) }
override DataFlow::SourceNode getOutput() { result = this }
}
from Replacement primary, Replacement supplementary, string message, string metachar
where
primary.escapes(metachar, _) and

View File

@@ -7,7 +7,7 @@
* @id js/loop-bound-injection
* @tags security
* external/cwe/cwe-834
* @precision medium
* @precision high
*/
import javascript

View File

@@ -14,17 +14,19 @@ import PortalExitSource
import PortalEntrySink
from
TaintTracking::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Portal p1,
Portal p2, DataFlow::FlowLabel lbl1, DataFlow::FlowLabel lbl2
TaintTracking::Configuration cfg, DataFlow::SourcePathNode source, DataFlow::SinkPathNode sink, Portal p1,
Portal p2, DataFlow::FlowLabel lbl1, DataFlow::FlowLabel lbl2, DataFlow::MidPathNode last
where
cfg.hasFlowPath(source, sink) and
cfg = source.getConfiguration() and
last = source.getASuccessor*() and
sink = last.getASuccessor() and
p1 = source.getNode().(PortalExitSource).getPortal() and
p2 = sink.getNode().(PortalEntrySink).getPortal() and
lbl1 = sink.getPathSummary().getStartLabel() and
lbl2 = sink.getPathSummary().getEndLabel() and
lbl1 = last.getPathSummary().getStartLabel() and
lbl2 = last.getPathSummary().getEndLabel() and
// avoid constructing infeasible paths
sink.getPathSummary().hasCall() = false and
sink.getPathSummary().hasReturn() = false and
last.getPathSummary().hasCall() = false and
last.getPathSummary().hasReturn() = false and
// restrict to steps flow function parameters to returns
p1.(ParameterPortal).getBasePortal() = p2.(ReturnPortal).getBasePortal() and
// restrict to data/taint flow

View File

@@ -11,10 +11,13 @@ import Configurations
import PortalExitSource
import SinkFromAnnotation
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Portal p
from DataFlow::Configuration cfg, DataFlow::SourcePathNode source, DataFlow::SinkPathNode sink,
Portal p, DataFlow::MidPathNode last
where
cfg.hasFlowPath(source, sink) and
cfg = source.getConfiguration() and
last = source.getASuccessor*() and
sink = last.getASuccessor() and
p = source.getNode().(PortalExitSource).getPortal() and
// avoid constructing infeasible paths
sink.getPathSummary().hasReturn() = false
select p.toString(), source.getPathSummary().getStartLabel().toString(), cfg.toString()
last.getPathSummary().hasReturn() = false
select p.toString(), last.getPathSummary().getStartLabel().toString(), cfg.toString()

View File

@@ -11,10 +11,13 @@ import Configurations
import PortalEntrySink
import SourceFromAnnotation
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Portal p
from DataFlow::Configuration cfg, DataFlow::SourcePathNode source, DataFlow::SinkPathNode sink,
Portal p, DataFlow::MidPathNode last
where
cfg.hasFlowPath(source, sink) and
cfg = source.getConfiguration() and
last = source.getASuccessor*() and
sink = last.getASuccessor() and
p = sink.getNode().(PortalEntrySink).getPortal() and
// avoid constructing infeasible paths
sink.getPathSummary().hasCall() = false
select p.toString(), sink.getPathSummary().getEndLabel().toString(), cfg.toString()
last.getPathSummary().hasCall() = false
select p.toString(), last.getPathSummary().getEndLabel().toString(), cfg.toString()

View File

@@ -1,51 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
On some platforms, the builtin function <code>parseInt</code> parses strings starting with the digit
<code>0</code> as octal values (unless an explicit radix is provided). This can lead to unexpected
results when parsing decimal numbers that may be zero-padded, such as dates.
</p>
</overview>
<recommendation>
<p>
Provide an explicit radix as the second parameter to <code>parseInt</code>.
</p>
</recommendation>
<example>
<p>
In the following example, <code>parseInt</code> is used to convert the contents of a field in an HTML
form to a number:
</p>
<sample src="examples/ParseIntRadix.js" />
<p>
Now assume that a user has entered a zero-padded decimal number, say <code>09</code>, into the form.
Since the first digit is a zero, older versions of <code>parseInt</code> interpret this value as an
octal number. When they then encounter <code>9</code> (which is not an octal digit), they will stop
parsing and discard the rest of the string, returning the value <code>0</code>, which is probably not
what was expected.
</p>
<p>
To avoid this problem, an explicit radix parameter should be parsed as follows:
</p>
<sample src="examples/ParseIntRadixGood.js" />
</example>
<references>
<li>D. Crockford, <i>JavaScript: The Good Parts</i>, Appendix A.7. O'Reilly, 2008.</li>
</references>
</qhelp>

View File

@@ -1,21 +0,0 @@
/**
* @name Call to parseInt without radix
* @description Calls to the 'parseInt' function should always specify a radix to avoid accidentally
* parsing a number as octal.
* @kind problem
* @problem.severity recommendation
* @id js/parseint-without-radix
* @tags reliability
* maintainability
* external/cwe/cwe-676
* @precision very-high
* @deprecated This is no longer a problem with modern browsers. Deprecated since 1.17.
*/
import javascript
from DataFlow::CallNode parseInt
where
parseInt = DataFlow::globalVarRef("parseInt").getACall() and
parseInt.getNumArgument() = 1
select parseInt, "Missing radix parameter."

View File

@@ -0,0 +1,43 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
JavaScript functions that do not return an expression will implicitly return
<code>undefined</code>. Using the implicit return value from such a function
is not an error in itself, but it is a pattern indicating that some
misunderstanding has occurred.
</p>
</overview>
<recommendation>
<p>
Do not use the return value from a function that does not return an expression.
</p>
</recommendation>
<example>
<p>
In the example below, the function <code>renderText</code> is used to render
text through side effects, and the function does not return an expression.
However, the programmer still uses the return value from
<code>renderText</code> as if the function returned an expression, which is
clearly an error.
</p>
<sample src="examples/UseOfReturnlessFunction.js" />
<p>
The program can be fixed either by removing the use of the value returned by
<code>renderText</code>, or by changing the <code>renderText</code> function
to return an expression.
</p>
</example>
<references>
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return">Return</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,168 @@
/**
* @name Use of returnless function
* @description Using the return value of a function that does not return an expression is indicative of a mistake.
* @kind problem
* @problem.severity warning
* @id js/use-of-returnless-function
* @tags maintainability
* correctness
* @precision high
*/
import javascript
import Declarations.UnusedVariable
import Expressions.ExprHasNoEffect
import Statements.UselessConditional
predicate returnsVoid(Function f) {
not f.isGenerator() and
not f.isAsync() and
not exists(f.getAReturnedExpr())
}
predicate isStub(Function f) {
f.getBody().(BlockStmt).getNumChild() = 0
or
f instanceof ExternalDecl
}
/**
* Holds if `e` is in a syntactic context where it likely is fine that the value of `e` comes from a call to a returnless function.
*/
predicate benignContext(Expr e) {
inVoidContext(e) or
// A return statement is often used to just end the function.
e = any(Function f).getAReturnedExpr()
or
exists(ConditionalExpr cond | cond.getABranch() = e and benignContext(cond))
or
exists(LogicalBinaryExpr bin | bin.getAnOperand() = e and benignContext(bin))
or
exists(Expr parent | parent.getUnderlyingValue() = e and benignContext(parent))
or
any(VoidExpr voidExpr).getOperand() = e
or
// weeds out calls inside HTML-attributes.
e.getParent() instanceof CodeInAttribute or
// and JSX-attributes.
e = any(JSXAttribute attr).getValue() or
exists(AwaitExpr await | await.getOperand() = e and benignContext(await))
or
// Avoid double reporting with js/trivial-conditional
isExplicitConditional(_, e)
or
// Avoid double reporting with js/comparison-between-incompatible-types
any(Comparison binOp).getAnOperand() = e
or
// Avoid double reporting with js/property-access-on-non-object
any(PropAccess ac).getBase() = e
or
// Avoid double-reporting with js/unused-local-variable
exists(VariableDeclarator v | v.getInit() = e and v.getBindingPattern().getVariable() instanceof UnusedLocal)
or
// Avoid double reporting with js/call-to-non-callable
any(InvokeExpr invoke).getCallee() = e
or
// arguments to Promise.resolve (and promise library variants) are benign.
e = any(ResolvedPromiseDefinition promise).getValue().asExpr()
}
predicate oneshotClosure(InvokeExpr call) {
call.getCallee().getUnderlyingValue() instanceof ImmediatelyInvokedFunctionExpr
}
predicate alwaysThrows(Function f) {
exists(ReachableBasicBlock entry, DataFlow::Node throwNode |
entry = f.getEntryBB() and
throwNode.asExpr() = any(ThrowStmt t).getExpr() and
entry.dominates(throwNode.getBasicBlock())
)
}
/**
* Holds if the last statement in the function is flagged by the js/useless-expression query.
*/
predicate lastStatementHasNoEffect(Function f) {
hasNoEffect(f.getExit().getAPredecessor())
}
/**
* Holds if `func` is a callee of `call`, and all possible callees of `call` never return a value.
*/
predicate callToVoidFunction(DataFlow::CallNode call, Function func) {
not call.isIncomplete() and
func = call.getACallee() and
forall(Function f | f = call.getACallee() |
returnsVoid(f) and not isStub(f) and not alwaysThrows(f)
)
}
/**
* Holds if `name` is the name of a method from `Array.prototype` or Lodash,
* where that method takes a callback as parameter,
* and the callback is expected to return a value.
*/
predicate hasNonVoidCallbackMethod(string name) {
name = "every" or
name = "filter" or
name = "find" or
name = "findIndex" or
name = "flatMap" or
name = "map" or
name = "reduce" or
name = "reduceRight" or
name = "some" or
name = "sort"
}
DataFlow::SourceNode array(DataFlow::TypeTracker t) {
t.start() and result instanceof DataFlow::ArrayCreationNode
or
exists (DataFlow::TypeTracker t2 |
result = array(t2).track(t2, t)
)
}
DataFlow::SourceNode array() { result = array(DataFlow::TypeTracker::end()) }
/**
* Holds if `call` is an Array or Lodash method accepting a callback `func`,
* where the `call` expects a callback that returns an expression,
* but `func` does not return a value.
*/
predicate voidArrayCallback(DataFlow::CallNode call, Function func) {
hasNonVoidCallbackMethod(call.getCalleeName()) and
exists(int index |
index = min(int i | exists(call.getCallback(i))) and
func = call.getCallback(index).getFunction()
) and
returnsVoid(func) and
not isStub(func) and
not alwaysThrows(func) and
(
call.getReceiver().getALocalSource() = array()
or
call.getCalleeNode().getALocalSource() instanceof LodashUnderscore::Member
)
}
from DataFlow::CallNode call, Function func, string name, string msg
where
(
callToVoidFunction(call, func) and
msg = "the $@ does not return anything, yet the return value is used." and
name = func.describe()
or
voidArrayCallback(call, func) and
msg = "the $@ does not return anything, yet the return value from the call to " + call.getCalleeName() + " is used." and
name = "callback function"
) and
not benignContext(call.asExpr()) and
not lastStatementHasNoEffect(func) and
// anonymous one-shot closure. Those are used in weird ways and we ignore them.
not oneshotClosure(call.asExpr())
select
call, msg, func, name

View File

@@ -16,6 +16,7 @@ import javascript
import semmle.javascript.RestrictedLocations
import semmle.javascript.dataflow.Refinements
import semmle.javascript.DefensiveProgramming
import UselessConditional
/**
* Gets the unique definition of `v`.
@@ -123,22 +124,6 @@ predicate whitelist(Expr e) {
isConstantBooleanReturnValue(e)
}
/**
* Holds if `e` is part of a conditional node `cond` that evaluates
* `e` and checks its value for truthiness, and the return value of `e`
* is not used for anything other than this truthiness check.
*/
predicate isExplicitConditional(ASTNode cond, Expr e) {
e = cond.(IfStmt).getCondition()
or
e = cond.(LoopStmt).getTest()
or
e = cond.(ConditionalExpr).getCondition()
or
isExplicitConditional(_, cond) and
e = cond.(Expr).getUnderlyingValue().(LogicalBinaryExpr).getAnOperand()
}
/**
* Holds if `e` is part of a conditional node `cond` that evaluates
* `e` and checks its value for truthiness.

View File

@@ -0,0 +1,21 @@
/**
* Provides predicates for working with useless conditionals.
*/
import javascript
/**
* Holds if `e` is part of a conditional node `cond` that evaluates
* `e` and checks its value for truthiness, and the return value of `e`
* is not used for anything other than this truthiness check.
*/
predicate isExplicitConditional(ASTNode cond, Expr e) {
e = cond.(IfStmt).getCondition()
or
e = cond.(LoopStmt).getTest()
or
e = cond.(ConditionalExpr).getCondition()
or
isExplicitConditional(_, cond) and
e = cond.(Expr).getUnderlyingValue().(LogicalBinaryExpr).getAnOperand()
}

View File

@@ -0,0 +1,9 @@
var stage = require("./stage")
function renderText(text, id) {
document.getElementById(id).innerText = text;
}
var text = renderText("Two households, both alike in dignity", "scene");
stage.show(text);

View File

@@ -0,0 +1,4 @@
- description: Standard LGTM queries for JavaScript, including ones not displayed by default
- qlpack: codeql-javascript
- apply: lgtm-selectors.yml
from: codeql-suite-helpers

View File

@@ -0,0 +1,4 @@
- description: Standard LGTM queries for JavaScript
- apply: codeql-suites/javascript-lgtm-full.qls
- apply: lgtm-displayed-only.yml
from: codeql-suite-helpers

View File

@@ -47,97 +47,6 @@ class RelevantFunction extends Function {
}
}
/**
* Holds if `name` is a the name of an external module.
*/
predicate isExternalLibrary(string name) {
// Mentioned in package.json
any(Dependency dep).info(name, _) or
// Node.js built-in
name = "assert" or
name = "async_hooks" or
name = "child_process" or
name = "cluster" or
name = "crypto" or
name = "dns" or
name = "domain" or
name = "events" or
name = "fs" or
name = "http" or
name = "http2" or
name = "https" or
name = "inspector" or
name = "net" or
name = "os" or
name = "path" or
name = "perf_hooks" or
name = "process" or
name = "punycode" or
name = "querystring" or
name = "readline" or
name = "repl" or
name = "stream" or
name = "string_decoder" or
name = "timer" or
name = "tls" or
name = "trace_events" or
name = "tty" or
name = "dgram" or
name = "url" or
name = "util" or
name = "v8" or
name = "vm" or
name = "worker_threads" or
name = "zlib"
}
/**
* Holds if the global variable `name` is defined externally.
*/
predicate isExternalGlobal(string name) {
exists(ExternalGlobalDecl decl | decl.getName() = name)
or
exists(Dependency dep |
// If name is never assigned anywhere, and it coincides with a dependency,
// it's most likely coming from there.
dep.info(name, _) and
not exists(Assignment assign | assign.getLhs().(GlobalVarAccess).getName() = name)
)
or
name = "_"
}
/**
* Gets a node that was derived from an import of `moduleName`.
*
* This is a rough approximation as it follows all property reads, invocations,
* and callbacks, so some of these might refer to internal objects.
*
* Additionally, we don't recognize when a project imports another file in the
* same project using its module name (for example import "vscode" from inside the vscode project).
*/
SourceNode externalNode() {
exists(string moduleName |
result = moduleImport(moduleName) and
isExternalLibrary(moduleName)
)
or
exists(string name |
result = globalVarRef(name) and
isExternalGlobal(name)
)
or
result = DOM::domValueRef()
or
result = jquery()
or
result = externalNode().getAPropertyRead()
or
result = externalNode().getAnInvocation()
or
result = externalNode().(InvokeNode).getCallback(_).getParameter(_)
}
/**
* Gets a data flow node that can be resolved to a function, usually a callback.
*
@@ -167,7 +76,7 @@ SourceNode nodeLeadingToInvocation() {
result.flowsTo(arg)
)
or
exists(AdditionalPartialInvokeNode invoke, Node arg |
exists(PartialInvokeNode invoke, Node arg |
invoke.isPartialArgument(arg, _, _) and
result.flowsTo(arg)
)
@@ -192,49 +101,15 @@ class ResolvableCall extends RelevantInvoke {
}
}
/**
* A call site that is believed to call an external function.
*/
class ExternalCall extends RelevantInvoke {
ExternalCall() {
not this instanceof ResolvableCall and // avoid double counting
(
// Call to modelled external library
this = externalNode()
or
// 'require' call or similar
this = moduleImport(_)
or
// Resolved to externs file
exists(this.(InvokeNode).getACallee(1))
or
// Modelled as taint step but isn't from an NPM module, for example, `substring` or `push`.
exists(TaintTracking::AdditionalTaintStep step |
step.step(_, this)
or
step.step(this.getAnArgument(), _)
)
)
}
}
/**
* A call site that could not be resolved.
*/
class UnresolvableCall extends RelevantInvoke {
UnresolvableCall() {
not this instanceof ResolvableCall and
not this instanceof ExternalCall
not this instanceof ResolvableCall
}
}
/**
* A call that is believed to call a function within the same project.
*/
class NonExternalCall extends RelevantInvoke {
NonExternalCall() { not this instanceof ExternalCall }
}
/**
* A function with at least one call site.
*/

View File

@@ -0,0 +1,4 @@
name: codeql-javascript
version: 0.0.0
dbscheme: semmlecode.javascript.dbscheme
suites: codeql-suites

View File

@@ -0,0 +1,97 @@
/**
* Provides classes for reasoning about character escapes in literals.
*/
import javascript
module CharacterEscapes {
/**
* Provides sets of characters (as strings) with specific string/regexp characteristics.
*/
private module Sets {
string sharedEscapeChars() { result = "fnrtvux0\\" }
string regexpAssertionChars() { result = "bB" }
string regexpCharClassChars() { result = "cdDpPsSwW" }
string regexpBackreferenceChars() { result = "123456789k" }
string regexpMetaChars() { result = "^$*+?.()|{}[]-" }
}
/**
* Gets the `i`th character of `raw`, which is preceded by an uneven number of backslashes.
*/
bindingset[raw]
string getAnEscapedCharacter(string raw, int i) {
result = raw.regexpFind("(?<=(^|[^\\\\])\\\\(\\\\{2}){0,10}).", _, i)
}
/**
* Holds if `n` is delimited by `delim` and contains `rawStringNode` with the raw string value `raw`.
*/
private predicate hasRawStringAndQuote(
DataFlow::ValueNode n, string delim, ASTNode rawStringNode, string raw
) {
rawStringNode = n.asExpr() and
raw = rawStringNode.(StringLiteral).getRawValue() and
delim = raw.charAt(0)
or
rawStringNode = n.asExpr().(TemplateLiteral).getAnElement() and
raw = rawStringNode.(TemplateElement).getRawValue() and
delim = "`"
or
rawStringNode = n.asExpr() and
raw = rawStringNode.(RegExpLiteral).getRawValue() and
delim = "/"
}
/**
* Gets a character in `n` that is preceded by a single useless backslash.
*
* The character is the `i`th character of `rawStringNode`'s raw string value.
*/
string getAnIdentityEscapedCharacter(DataFlow::Node n, ASTNode rawStringNode, int i) {
exists(string delim, string raw, string additionalEscapeChars |
hasRawStringAndQuote(n, delim, rawStringNode, raw) and
if rawStringNode instanceof RegExpLiteral
then
additionalEscapeChars = Sets::regexpMetaChars() + Sets::regexpAssertionChars() + Sets::regexpCharClassChars() +
Sets::regexpBackreferenceChars()
else additionalEscapeChars = "b"
|
result = getAnEscapedCharacter(raw, i) and
not result = (Sets::sharedEscapeChars() + delim + additionalEscapeChars).charAt(_)
)
}
/**
* Holds if `src` likely contains a regular expression mistake, to be reported by `js/useless-regexp-character-escape`.
*/
predicate hasALikelyRegExpPatternMistake(RegExpPatternSource src) {
exists(getALikelyRegExpPatternMistake(src, _, _, _))
}
/**
* Gets a character in `n` that is preceded by a single useless backslash, resulting in a likely regular expression mistake explained by `mistake`.
*
* The character is the `i`th character of the raw string value of `rawStringNode`.
*/
string getALikelyRegExpPatternMistake(
RegExpPatternSource src, string mistake, ASTNode rawStringNode, int i
) {
result = getAnIdentityEscapedCharacter(src, rawStringNode, i) and
(
result = Sets::regexpAssertionChars().charAt(_) and mistake = "is not an assertion"
or
result = Sets::regexpCharClassChars().charAt(_) and mistake = "is not a character class"
or
result = Sets::regexpBackreferenceChars().charAt(_) and mistake = "is not a backreference"
or
// conservative formulation: we do not know in general if the sequence is enclosed in a character class `[...]`
result = Sets::regexpMetaChars().charAt(_) and
mistake = "may still represent a meta-character"
)
}
}

View File

@@ -123,6 +123,14 @@ class ClassOrInterface extends @classorinterface, TypeParameterized {
* Anonymous classes and interfaces do not have a canonical name.
*/
TypeName getTypeName() { result.getADefinition() = this }
/**
* Gets the ClassOrInterface corresponding to either a super type or an implemented interface.
*/
ClassOrInterface getASuperTypeDeclaration() {
this.getSuperClass().(VarAccess).getVariable().getADeclaration() = result.getIdentifier() or
this.getASuperInterface().(LocalTypeAccess).getLocalTypeName().getADeclaration() = result.getIdentifier()
}
}
/**
@@ -1051,6 +1059,12 @@ class FieldDeclaration extends MemberDeclaration, @field {
/** Holds if this is a TypeScript field marked as definitely assigned with the `!` operator. */
predicate hasDefiniteAssignmentAssertion() { hasDefiniteAssignmentAssertion(this) }
override predicate isAmbient() {
hasDeclareKeyword(this)
or
getParent().isAmbient()
}
}
/**

View File

@@ -248,4 +248,25 @@ module Closure {
DataFlow::SourceNode moduleImport(string moduleName) {
getClosureNamespaceFromSourceNode(result) = moduleName
}
/**
* A call to `goog.bind`, as a partial function invocation.
*/
private class BindCall extends DataFlow::PartialInvokeNode::Range, DataFlow::CallNode {
BindCall() { this = moduleImport("goog.bind").getACall() }
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
index >= 0 and
callback = getArgument(0) and
argument = getArgument(index + 2)
}
override DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) {
boundArgs = getNumArgument() - 2 and
callback = getArgument(0) and
result = this
}
override DataFlow::Node getBoundReceiver() { result = getArgument(1) }
}
}

View File

@@ -14,6 +14,9 @@ abstract class SystemCommandExecution extends DataFlow::Node {
/** Gets an argument to this execution that specifies the command. */
abstract DataFlow::Node getACommandArgument();
/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { none() }
/**
* Gets an argument to this command execution that specifies the argument list
* to the command.
@@ -36,6 +39,14 @@ abstract class FileSystemAccess extends DataFlow::Node {
* sanitization to prevent the path arguments from traversing outside the root folder.
*/
DataFlow::Node getRootPathArgument() { none() }
/**
* Holds if this file system access will reject paths containing upward navigation
* segments (`../`).
*
* `argument` should refer to the relevant path argument or root path argument.
*/
predicate isUpwardNavigationRejected(DataFlow::Node argument) { none() }
}
/**

View File

@@ -641,4 +641,4 @@ class OriginalExportDeclaration extends ExportDeclaration {
result = this.(ExportDefaultDeclaration).getSourceNode(name) or
result = this.(ExportNamedDeclaration).getSourceNode(name)
}
}
}

View File

@@ -2645,3 +2645,15 @@ class OptionalChainRoot extends ChainElem {
*/
OptionalUse getAnOptionalUse() { result = optionalUse }
}
/**
* An `import.meta` expression.
*
* Example:
* ```js
* let url = import.meta.url;
* ```
*/
class ImportMetaExpr extends @importmetaexpr, Expr {
override predicate isImpure() { none() }
}

View File

@@ -137,6 +137,11 @@ private predicate isGeneratedHtml(File f) {
e.getAttributeByName("name").getValue() = "generator"
)
or
exists(HTML::CommentNode comment |
comment.getText().regexpMatch("\\s*Generated by [\\w-]+ \\d+\\.\\d+\\.\\d+\\s*") and
comment.getFile() = f
)
or
20 < countStartingHtmlElements(f, _)
}

View File

@@ -914,20 +914,49 @@ class TypeofTypeExpr extends @typeoftypeexpr, TypeExpr {
}
/**
* A type of form `E is T` where `E` is a parameter name or `this`, and `T` is a type.
* A function return type that refines the type of one of its parameters or `this`.
*
* This can only occur as the return type of a function type.
* Examples:
* ```js
* function f(x): x is string {}
* function f(x): asserts x is string {}
* function f(x): asserts x {}
* ```
*/
class IsTypeExpr extends @istypeexpr, TypeExpr {
class PredicateTypeExpr extends @predicatetypeexpr, TypeExpr {
/**
* Gets the parameter name or `this` token `E` in `E is T`.
*/
VarTypeAccess getParameterName() { result = this.getChildTypeExpr(0) }
/**
* Gets the type `T` in `E is T`.
* Gets the type `T` in `E is T` or `asserts E is T`.
*
* Has no results for types of form `asserts E`.
*/
TypeExpr getPredicateType() { result = this.getChildTypeExpr(1) }
/**
* Holds if this is a type of form `asserts E is T` or `asserts E`.
*/
predicate hasAssertsKeyword() {
hasAssertsKeyword(this)
}
}
/**
* A function return type of form `x is T` or `asserts x is T`.
*
* Examples:
* ```js
* function f(x): x is string {}
* function f(x): asserts x is string {}
* ```
*/
class IsTypeExpr extends PredicateTypeExpr {
IsTypeExpr() {
exists(getPredicateType())
}
}
/**
@@ -966,15 +995,16 @@ class ReadonlyTypeExpr extends @readonlytypeexpr, TypeExpr {
*
* This can occur as
* - part of the operand to a `typeof` type, or
* - as the first operand to an `is` type.
* - as the first operand to a predicate type
*
* For example, it may occur as the `E` in these examples:
* ```
* var x : typeof E
* function f(...) : E is T {}
* function f(...) : asserts E {}
* ```
*
* In the latter case, this may also refer to the pseudo-variable `this`.
* In the latter two cases, this may also refer to the pseudo-variable `this`.
*/
class VarTypeAccess extends @vartypeaccess, TypeExpr { }

View File

@@ -0,0 +1,44 @@
/**
* Provides machinery for performing backward data-flow exploration.
*
* Importing this module effectively makes all data-flow and taint-tracking configurations
* ignore their `isSource` predicate. Instead, flow is tracked from any _initial node_ (that is,
* a node without incoming flow) to a sink node. All initial nodes are then treated as source
* nodes.
*
* Data-flow exploration cannot be used with configurations depending on other configurations.
*
* NOTE: This library should only be used for debugging, not in production code. Backward
* exploration in particular does not scale on non-trivial code bases and hence is of limited
* usefulness as it stands.
*/
import javascript
private class BackwardExploringConfiguration extends DataFlow::Configuration {
DataFlow::Configuration cfg;
BackwardExploringConfiguration() {
this = cfg
}
override predicate isSource(DataFlow::Node node) { any() }
override predicate isSource(DataFlow::Node node, DataFlow::FlowLabel lbl) { any() }
override predicate hasFlow(DataFlow::Node source, DataFlow::Node sink) {
exists(DataFlow::PathNode src, DataFlow::PathNode snk | hasFlowPath(src, snk) |
source = src.getNode() and
sink = snk.getNode()
)
}
override predicate hasFlowPath(DataFlow::SourcePathNode source, DataFlow::SinkPathNode sink) {
exists(DataFlow::MidPathNode first |
source.getConfiguration() = this and
source.getASuccessor() = first and
not exists(DataFlow::MidPathNode mid | mid.getASuccessor() = first) and
first.getASuccessor*() = sink
)
}
}

View File

@@ -432,18 +432,6 @@ abstract class AdditionalSink extends DataFlow::Node {
predicate isSinkFor(Configuration cfg, FlowLabel lbl) { none() }
}
/**
* An invocation that is modeled as a partial function application.
*
* This contributes additional argument-passing flow edges that should be added to all data flow configurations.
*/
abstract class AdditionalPartialInvokeNode extends DataFlow::InvokeNode {
/**
* Holds if `argument` is passed as argument `index` to the function in `callback`.
*/
abstract predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index);
}
/**
* Additional flow step to model flow from import specifiers into the SSA variable
* corresponding to the imported variable.
@@ -457,45 +445,6 @@ private class FlowStepThroughImport extends AdditionalFlowStep, DataFlow::ValueN
}
}
/**
* A partial call through the built-in `Function.prototype.bind`.
*/
private class BindPartialCall extends AdditionalPartialInvokeNode, DataFlow::MethodCallNode {
BindPartialCall() { getMethodName() = "bind" }
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
callback = getReceiver() and
argument = getArgument(index + 1)
}
}
/**
* A partial call through `_.partial`.
*/
private class LodashPartialCall extends AdditionalPartialInvokeNode {
LodashPartialCall() { this = LodashUnderscore::member("partial").getACall() }
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
callback = getArgument(0) and
argument = getArgument(index + 1)
}
}
/**
* A partial call through `ramda.partial`.
*/
private class RamdaPartialCall extends AdditionalPartialInvokeNode {
RamdaPartialCall() { this = DataFlow::moduleMember("ramda", "partial").getACall() }
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
callback = getArgument(0) and
exists(DataFlow::ArrayCreationNode array |
array.flowsTo(getArgument(1)) and
argument = array.getElement(index)
)
}
}
/**
* Holds if there is a flow step from `pred` to `succ` described by `summary`
* under configuration `cfg`.
@@ -505,6 +454,7 @@ private class RamdaPartialCall extends AdditionalPartialInvokeNode {
private predicate basicFlowStep(
DataFlow::Node pred, DataFlow::Node succ, PathSummary summary, DataFlow::Configuration cfg
) {
isLive() and
isRelevantForward(pred, cfg) and
(
// Local flow
@@ -939,9 +889,9 @@ private predicate flowsTo(
PathNode flowsource, DataFlow::Node source, SinkPathNode flowsink, DataFlow::Node sink,
DataFlow::Configuration cfg
) {
flowsource = MkPathNode(source, cfg, _) and
flowsource.wraps(source, cfg) and
flowsink = flowsource.getASuccessor*() and
flowsink = MkPathNode(sink, id(cfg), _)
flowsink.wraps(sink, id(cfg))
}
/**
@@ -984,19 +934,45 @@ private predicate onPath(DataFlow::Node nd, DataFlow::Configuration cfg, PathSum
}
/**
* Holds if `cfg` has at least one source and at least one sink.
* Holds if there is a configuration that has at least one source and at least one sink.
*/
pragma[noinline]
private predicate isLive(DataFlow::Configuration cfg) { isSource(_, cfg, _) and isSink(_, cfg, _) }
private predicate isLive() { exists(DataFlow::Configuration cfg | isSource(_, cfg, _) and isSink(_, cfg, _)) }
/**
* A data flow node on an inter-procedural path from a source.
*/
private newtype TPathNode =
MkPathNode(DataFlow::Node nd, DataFlow::Configuration cfg, PathSummary summary) {
isLive(cfg) and
MkSourceNode(DataFlow::Node nd, DataFlow::Configuration cfg) { isSourceNode(nd, cfg, _) }
or
MkMidNode(DataFlow::Node nd, DataFlow::Configuration cfg, PathSummary summary) {
isLive() and
onPath(nd, cfg, summary)
}
or
MkSinkNode(DataFlow::Node nd, DataFlow::Configuration cfg) { isSinkNode(nd, cfg, _) }
/**
* Holds if `nd` is a source node for configuration `cfg`, and there is a path from `nd` to a sink
* with the given `summary`.
*/
private predicate isSourceNode(DataFlow::Node nd, DataFlow::Configuration cfg, PathSummary summary) {
exists(FlowLabel lbl | summary = PathSummary::level(lbl) |
isSource(nd, cfg, lbl) and
isLive() and
onPath(nd, cfg, summary)
)
}
/**
* Holds if `nd` is a sink node for configuration `cfg`, and there is a path from a source to `nd`
* with the given `summary`.
*/
private predicate isSinkNode(DataFlow::Node nd, DataFlow::Configuration cfg, PathSummary summary) {
isSink(nd, cfg, summary.getEndLabel()) and
isLive() and
onPath(nd, cfg, summary)
}
/**
* Maps `cfg` to itself.
@@ -1008,48 +984,46 @@ bindingset[cfg, result]
private DataFlow::Configuration id(DataFlow::Configuration cfg) { result >= cfg and cfg >= result }
/**
* A data flow node on an inter-procedural path from a source to a sink.
* A data-flow node on an inter-procedural path from a source to a sink.
*
* A path node is a triple `(nd, cfg, summary)` where `nd` is a data flow node and `cfg`
* is a data flow tracking configuration such that `nd` is on a path from a source to a
* sink under `cfg` summarized by `summary`.
* A path node wraps a data-flow node `nd` and a data-flow configuration `cfg` such that `nd` is
* on a path from a source to a sink under `cfg`.
*
* There are three kinds of path nodes:
*
* - source nodes: wrapping a source node and a configuration such that there is a path from that
* source to some sink under the configuration;
* - sink nodes: wrapping a sink node and a configuration such that there is a path from some source
* to that sink under the configuration;
* - mid nodes: wrapping a node, a configuration and a path summary such that there is a path from
* some source to the node with the given summary that can be extended to a path to some sink node,
* all under the configuration.
*/
class PathNode extends TPathNode {
DataFlow::Node nd;
DataFlow::Configuration cfg;
PathSummary summary;
Configuration cfg;
PathNode() { this = MkPathNode(nd, cfg, summary) }
PathNode() {
this = MkSourceNode(nd, cfg) or
this = MkMidNode(nd, cfg, _) or
this = MkSinkNode(nd, cfg)
}
/** Gets the underlying data flow node of this path node. */
DataFlow::Node getNode() { result = nd }
/** Holds if this path node wraps data-flow node `nd` and configuration `c`. */
predicate wraps(DataFlow::Node n, DataFlow::Configuration c) {
nd = n and cfg = c
}
/** Gets the underlying data flow tracking configuration of this path node. */
/** Gets the underlying configuration of this path node. */
DataFlow::Configuration getConfiguration() { result = cfg }
/** Gets the summary of the path underlying this path node. */
PathSummary getPathSummary() { result = summary }
/**
* Gets a successor node of this path node, including hidden nodes.
*/
private PathNode getASuccessorInternal() {
exists(DataFlow::Node succ, PathSummary newSummary |
flowStep(nd, id(cfg), succ, newSummary) and
result = MkPathNode(succ, id(cfg), summary.append(newSummary))
)
}
/**
* Gets a successor of this path node, if it is a hidden node.
*/
private PathNode getAHiddenSuccessor() {
isHidden() and
result = getASuccessorInternal()
}
/** Gets the underlying data-flow node of this path node. */
DataFlow::Node getNode() { result = nd }
/** Gets a successor node of this path node. */
PathNode getASuccessor() { result = getASuccessorInternal().getAHiddenSuccessor*() }
final PathNode getASuccessor() {
result = getASuccessor(this)
}
/** Gets a textual representation of this path node. */
string toString() { result = nd.toString() }
@@ -1066,6 +1040,68 @@ class PathNode extends TPathNode {
) {
nd.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
}
/** Gets the mid node corresponding to `src`. */
private MidPathNode initialMidNode(SourcePathNode src) {
exists(DataFlow::Node nd, Configuration cfg, PathSummary summary |
result.wraps(nd, cfg, summary) and
src = MkSourceNode(nd, cfg) and
isSourceNode(nd, cfg, summary)
)
}
/** Gets the mid node corresponding to `snk`. */
private MidPathNode finalMidNode(SinkPathNode snk) {
exists(DataFlow::Node nd, Configuration cfg, PathSummary summary |
result.wraps(nd, cfg, summary) and
snk = MkSinkNode(nd, cfg) and
isSinkNode(nd, cfg, summary)
)
}
/**
* Gets a node to which data from `nd` may flow in one step.
*/
private PathNode getASuccessor(PathNode nd) {
// source node to mid node
result = initialMidNode(nd)
or
// mid node to mid node
exists(Configuration cfg, DataFlow::Node predNd, PathSummary summary, DataFlow::Node succNd, PathSummary newSummary |
nd = MkMidNode(predNd, cfg, summary) and
flowStep(predNd, id(cfg), succNd, newSummary) and
result = MkMidNode(succNd, id(cfg), summary.append(newSummary))
)
or
// mid node to sink node
nd = finalMidNode(result)
}
private PathNode getASuccessorIfHidden(PathNode nd) {
nd.(MidPathNode).isHidden() and
result = getASuccessor(nd)
}
/**
* A path node corresponding to an intermediate node on a path from a source to a sink.
*
* A mid node is a triple `(nd, cfg, summary)` where `nd` is a data-flow node and `cfg`
* is a configuration such that `nd` is on a path from a source to a sink under `cfg`
* summarized by `summary`.
*/
class MidPathNode extends PathNode, MkMidNode {
PathSummary summary;
MidPathNode() { this = MkMidNode(nd, cfg, summary) }
/** Gets the summary of the path underlying this path node. */
PathSummary getPathSummary() { result = summary }
/** Holds if this path node wraps data-flow node `nd`, configuration `c` and summary `s`. */
predicate wraps(DataFlow::Node n, DataFlow::Configuration c, PathSummary s) {
nd = n and cfg = c and summary = s
}
/**
* Holds if this node is hidden from paths in path explanation queries, except
@@ -1085,20 +1121,15 @@ class PathNode extends TPathNode {
/**
* A path node corresponding to a flow source.
*/
class SourcePathNode extends PathNode {
SourcePathNode() {
exists(FlowLabel lbl |
summary = PathSummary::level(lbl) and
isSource(nd, cfg, lbl)
)
}
class SourcePathNode extends PathNode, MkSourceNode {
SourcePathNode() { this = MkSourceNode(nd, cfg) }
}
/**
* A path node corresponding to a flow sink.
*/
class SinkPathNode extends PathNode {
SinkPathNode() { isSink(nd, cfg, summary.getEndLabel()) }
class SinkPathNode extends PathNode, MkSinkNode {
SinkPathNode() { this = MkSinkNode(nd, cfg) }
}
/**
@@ -1107,11 +1138,51 @@ class SinkPathNode extends PathNode {
module PathGraph {
/** Holds if `nd` is a node in the graph of data flow path explanations. */
query predicate nodes(PathNode nd) {
not nd.isHidden() or
nd instanceof SourcePathNode or
nd instanceof SinkPathNode
not nd.(MidPathNode).isHidden()
}
/**
* Gets a node to which data from `nd` may flow in one step, skipping over hidden nodes.
*/
private PathNode succ0(PathNode nd) {
result = getASuccessorIfHidden*(nd.getASuccessor()) and
// skip hidden nodes
nodes(nd) and nodes(result)
}
/**
* Gets a node to which data from `nd` may flow in one step, where outgoing edges from intermediate
* nodes are merged with any incoming edge from a corresponding source node.
*
* For example, assume that `src` is a source node for `nd1`, which has `nd2` as its direct
* successor. Then `succ0` will yield two edges `src` &rarr; `nd1` and `nd1` &rarr; `nd2`,
* to which `succ1` will add the edge `src` &rarr; `nd2`.
*/
private PathNode succ1(PathNode nd) {
result = succ0(nd)
or
result = succ0(initialMidNode(nd))
}
/**
* Gets a node to which data from `nd` may flow in one step, where incoming edges into intermediate
* nodes are merged with any outgoing edge to a corresponding sink node.
*
* For example, assume that `snk` is a source node for `nd2`, which has `nd1` as its direct
* predecessor. Then `succ1` will yield two edges `nd1` &rarr; `nd2` and `nd2` &rarr; `snk`,
* while `succ2` will yield just one edge `nd1` &rarr; `snk`.
*/
private PathNode succ2(PathNode nd) {
result = succ1(nd)
or
succ1(nd) = finalMidNode(result)
}
/** Holds if `pred` &rarr; `succ` is an edge in the graph of data flow path explanations. */
query predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
query predicate edges(PathNode pred, PathNode succ) {
succ = succ2(pred) and
// skip over uninteresting edges
not succ = initialMidNode(pred) and
not pred = finalMidNode(succ)
}
}

View File

@@ -19,6 +19,7 @@
*/
import javascript
private import internal.CallGraphs
module DataFlow {
cached
@@ -117,14 +118,23 @@ module DataFlow {
int getIntValue() { result = asExpr().getIntValue() }
/** Gets a function value that may reach this node. */
FunctionNode getAFunctionValue() {
result.getAstNode() = analyze().getAValue().(AbstractCallable).getFunction()
final FunctionNode getAFunctionValue() {
CallGraph::getAFunctionReference(result, 0).flowsTo(this)
}
/** Gets a function value that may reach this node with the given `imprecision` level. */
final FunctionNode getAFunctionValue(int imprecision) {
CallGraph::getAFunctionReference(result, imprecision).flowsTo(this)
}
/**
* Gets a function value that may reach this node,
* possibly derived from a partial function invocation.
*/
final FunctionNode getABoundFunctionValue(int boundArgs) {
result = getAFunctionValue() and boundArgs = 0
or
exists(string name |
GlobalAccessPath::isAssignedInUniqueFile(name) and
GlobalAccessPath::fromRhs(result) = name and
GlobalAccessPath::fromReference(this) = name
)
CallGraph::getABoundFunctionReference(result, boundArgs).flowsTo(this)
}
/**
@@ -1415,6 +1425,8 @@ module DataFlow {
or
e instanceof NewTargetExpr
or
e instanceof ImportMetaExpr
or
e instanceof FunctionBindExpr
or
e instanceof TaggedTemplateExpr

View File

@@ -0,0 +1,42 @@
/**
* Provides machinery for performing forward data-flow exploration.
*
* Importing this module effectively makes all data-flow and taint-tracking configurations
* ignore their `isSink` predicate. Instead, flow is tracked from source nodes as far as
* possible, until a _terminal node_ (that is, a node without any outgoing flow) is reached.
* All terminal nodes are then treated as sink nodes.
*
* Data-flow exploration cannot be used with configurations depending on other configurations.
*
* NOTE: This library should only be used for debugging, not in production code.
*/
import javascript
private class ForwardExploringConfiguration extends DataFlow::Configuration {
DataFlow::Configuration cfg;
ForwardExploringConfiguration() {
this = cfg
}
override predicate isSink(DataFlow::Node node) { any() }
override predicate isSink(DataFlow::Node node, DataFlow::FlowLabel lbl) { any() }
override predicate hasFlow(DataFlow::Node source, DataFlow::Node sink) {
exists(DataFlow::PathNode src, DataFlow::PathNode snk | hasFlowPath(src, snk) |
source = src.getNode() and
sink = snk.getNode()
)
}
override predicate hasFlowPath(DataFlow::SourcePathNode source, DataFlow::SinkPathNode sink) {
exists(DataFlow::MidPathNode last |
source.getConfiguration() = this and
source.getASuccessor*() = last and
not last.getASuccessor() instanceof DataFlow::MidPathNode and
last.getASuccessor() = sink
)
}
}

View File

@@ -6,6 +6,7 @@
private import javascript
private import semmle.javascript.dependencies.Dependencies
private import internal.CallGraphs
/** A data flow node corresponding to an expression. */
class ExprNode extends DataFlow::ValueNode {
@@ -89,9 +90,43 @@ class InvokeNode extends DataFlow::SourceNode {
Function getEnclosingFunction() { result = getBasicBlock().getContainer() }
/** Gets a function passed as the `i`th argument of this invocation. */
/**
* Gets a function passed as the `i`th argument of this invocation.
*
* This predicate only performs local data flow tracking.
* Consider using `getABoundCallbackParameter` to handle interprocedural flow of callback functions.
*/
FunctionNode getCallback(int i) { result.flowsTo(getArgument(i)) }
/**
* Gets a parameter of a callback passed into this call.
*
* `callback` indicates which argument the callback passed into, and `param`
* is the index of the parameter in the callback function.
*
* For example, for the call below, `getABoundCallbackParameter(1, 0)` refers
* to the parameter `e` (the first parameter of the second callback argument):
* ```js
* addEventHandler("click", e => { ... })
* ```
*
* This predicate takes interprocedural data flow into account, as well as
* partial function applications such as `.bind`.
*
* For example, for the call below `getABoundCallbackParameter(1, 0)` returns the parameter `e`,
* (the first parameter of the second callback argument), since the first parameter of `foo`
* has been bound by the `bind` call:
* ```js
* function foo(x, e) { }
* addEventHandler("click", foo.bind(this, "value of x"))
* ```
*/
ParameterNode getABoundCallbackParameter(int callback, int param) {
exists(int boundArgs |
result = getArgument(callback).getABoundFunctionValue(boundArgs).getParameter(param + boundArgs)
)
}
/**
* Holds if the `i`th argument of this invocation is an object literal whose property
* `name` is set to `result`.
@@ -104,7 +139,7 @@ class InvokeNode extends DataFlow::SourceNode {
}
/** Gets an abstract value representing possible callees of this call site. */
AbstractValue getACalleeValue() { result = InvokeNode::getACalleeValue(this) }
final AbstractValue getACalleeValue() { result = getCalleeNode().analyze().getAValue() }
/**
* Gets a potential callee of this call site.
@@ -112,7 +147,7 @@ class InvokeNode extends DataFlow::SourceNode {
* To alter the call graph as seen by the interprocedural data flow libraries, override
* the `getACallee(int imprecision)` predicate instead.
*/
Function getACallee() { result = InvokeNode::getACallee(this) }
final Function getACallee() { result = getACallee(0) }
/**
* Gets a callee of this call site where `imprecision` is a heuristic measure of how
@@ -120,12 +155,15 @@ class InvokeNode extends DataFlow::SourceNode {
* imprecise analysis of global variables and is not, in fact, a viable callee at all.
*
* Callees with imprecision zero, in particular, have either been derived without
* considering global variables, or are calls to a global variable within the same file.
* considering global variables, or are calls to a global variable within the same file,
* or a global variable that has unique definition within the project.
*
* This predicate can be overridden to alter the call graph used by the interprocedural
* data flow libraries.
*/
Function getACallee(int imprecision) { result = InvokeNode::getACallee(this, imprecision) }
Function getACallee(int imprecision) {
result = CallGraph::getACallee(this, imprecision).getFunction()
}
/**
* Holds if the approximation of possible callees for this call site is
@@ -173,60 +211,6 @@ class InvokeNode extends DataFlow::SourceNode {
}
}
/** Auxiliary module used to cache a few related predicates together. */
cached
private module InvokeNode {
/** Gets an abstract value representing possible callees of `invk`. */
cached
AbstractValue getACalleeValue(InvokeNode invk) {
result = invk.getCalleeNode().analyze().getAValue()
}
/** Gets a potential callee of `invk` based on dataflow analysis results. */
private Function getACalleeFromDataflow(InvokeNode invk) {
result = getACalleeValue(invk).(AbstractCallable).getFunction()
}
/** Gets a potential callee of `invk`. */
cached
Function getACallee(InvokeNode invk) {
result = getACalleeFromDataflow(invk)
or
not exists(getACalleeFromDataflow(invk)) and
result = invk.(DataFlow::Impl::ExplicitInvokeNode).asExpr().(InvokeExpr).getResolvedCallee()
}
/**
* Gets a callee of `invk` where `imprecision` is a heuristic measure of how
* likely it is that `callee` is only suggested as a potential callee due to
* imprecise analysis of global variables and is not, in fact, a viable callee at all.
*
* Callees with imprecision zero, in particular, have either been derived without
* considering global variables, or are calls to a global variable within the same file.
*/
cached
Function getACallee(InvokeNode invk, int imprecision) {
result = getACallee(invk) and
(
// if global flow was used to derive the callee, we may be imprecise
if invk.isIndefinite("global")
then
// callees within the same file are probably genuine
result.getFile() = invk.getFile() and imprecision = 0
or
// calls to global functions declared in an externs file are fairly
// safe as well
result.inExternsFile() and imprecision = 1
or
// otherwise we make worst-case assumptions
imprecision = 2
else
// no global flow, so no imprecision
imprecision = 0
)
}
}
/** A data flow node corresponding to a function call without `new`. */
class CallNode extends InvokeNode {
override DataFlow::Impl::CallNodeDef impl;
@@ -311,6 +295,9 @@ DataFlow::SourceNode globalObjectRef() {
// DOM and service workers
result = globalVarRef("self")
or
// ECMAScript 2020
result = globalVarRef("globalThis")
or
// `require("global")`
result = moduleImport("global")
or
@@ -400,6 +387,9 @@ class ArrayLiteralNode extends DataFlow::ValueNode, DataFlow::SourceNode {
/** Gets an element of this array literal. */
DataFlow::ValueNode getAnElement() { result = DataFlow::valueNode(astNode.getAnElement()) }
/** Gets the initial size of this array. */
int getSize() { result = astNode.getSize() }
}
/** A data flow node corresponding to a `new Array()` or `Array()` invocation. */
@@ -417,6 +407,14 @@ class ArrayConstructorInvokeNode extends DataFlow::InvokeNode {
getNumArgument() > 1 and
result = getAnArgument()
}
/** Gets the initial size of the created array, if it can be determined. */
int getSize() {
if getNumArgument() = 1 then
result = getArgument(0).getIntValue()
else
result = count(getAnElement())
}
}
/**
@@ -437,6 +435,12 @@ class ArrayCreationNode extends DataFlow::ValueNode, DataFlow::SourceNode {
/** Gets an initial element of this array, if one if provided. */
DataFlow::ValueNode getAnElement() { result = getElement(_) }
/** Gets the initial size of the created array, if it can be determined. */
int getSize() {
result = this.(ArrayLiteralNode).getSize() or
result = this.(ArrayConstructorInvokeNode).getSize()
}
}
/**
@@ -657,6 +661,8 @@ class ClassNode extends DataFlow::SourceNode {
/**
* Gets a direct super class of this class.
*
* This predicate can be overridden to customize the class hierarchy.
*/
ClassNode getADirectSuperClass() { result.getAClassReference().flowsTo(getASuperClassNode()) }
@@ -686,10 +692,17 @@ class ClassNode extends DataFlow::SourceNode {
/**
* Gets a dataflow node that refers to this class object.
*
* This predicate can be overridden to customize the tracking of class objects.
*/
private DataFlow::SourceNode getAClassReference(DataFlow::TypeTracker t) {
DataFlow::SourceNode getAClassReference(DataFlow::TypeTracker t) {
t.start() and
result.(AnalyzedNode).getAValue() = getAbstractClassValue()
result.(AnalyzedNode).getAValue() = getAbstractClassValue() and
(
not CallGraph::isIndefiniteGlobal(result)
or
result.getAstNode().getFile() = this.getAstNode().getFile()
)
or
exists(DataFlow::TypeTracker t2 | result = getAClassReference(t2).track(t2, t))
}
@@ -698,14 +711,16 @@ class ClassNode extends DataFlow::SourceNode {
* Gets a dataflow node that refers to this class object.
*/
cached
DataFlow::SourceNode getAClassReference() {
final DataFlow::SourceNode getAClassReference() {
result = getAClassReference(DataFlow::TypeTracker::end())
}
/**
* Gets a dataflow node that refers to an instance of this class.
*
* This predicate can be overridden to customize the tracking of class instances.
*/
private DataFlow::SourceNode getAnInstanceReference(DataFlow::TypeTracker t) {
DataFlow::SourceNode getAnInstanceReference(DataFlow::TypeTracker t) {
result = getAClassReference(t.continue()).getAnInstantiation()
or
t.start() and
@@ -740,10 +755,17 @@ class ClassNode extends DataFlow::SourceNode {
* Gets a dataflow node that refers to an instance of this class.
*/
cached
DataFlow::SourceNode getAnInstanceReference() {
final DataFlow::SourceNode getAnInstanceReference() {
result = getAnInstanceReference(DataFlow::TypeTracker::end())
}
/**
* Gets an access to a static member of this class.
*/
DataFlow::PropRead getAStaticMemberAccess(string name) {
result = getAClassReference().getAPropertyRead(name)
}
/**
* Holds if this class is exposed in the global scope through the given qualified name.
*/
@@ -975,3 +997,140 @@ module ClassNode {
}
}
}
/**
* A data flow node that performs a partial function application.
*
* Examples:
* ```js
* fn.bind(this)
* x.method.bind(x)
* _.partial(fn, x, y, z)
* ```
*/
class PartialInvokeNode extends DataFlow::Node {
PartialInvokeNode::Range range;
PartialInvokeNode() { this = range }
/**
* Holds if `argument` is passed as argument `index` to the function in `callback`.
*/
predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
range.isPartialArgument(callback, argument, index)
}
/**
* Gets a node referring to a bound version of `callback` with `boundArgs` arguments bound.
*/
DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) {
result = range.getBoundFunction(callback, boundArgs)
}
/**
* Gets the node holding the receiver to be passed to the bound function, if specified.
*/
DataFlow::Node getBoundReceiver() { result = range.getBoundReceiver() }
}
module PartialInvokeNode {
/**
* A data flow node that performs a partial function application.
*/
abstract class Range extends DataFlow::Node {
/**
* Holds if `argument` is passed as argument `index` to the function in `callback`.
*/
predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) { none() }
/**
* Gets a node referring to a bound version of `callback` with `boundArgs` arguments bound.
*/
DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) { none() }
/**
* Gets the node holding the receiver to be passed to the bound function, if specified.
*/
DataFlow::Node getBoundReceiver() { none() }
}
/**
* A partial call through the built-in `Function.prototype.bind`.
*/
private class BindPartialCall extends PartialInvokeNode::Range, DataFlow::MethodCallNode {
BindPartialCall() {
getMethodName() = "bind" and
// Avoid overlap with angular.bind and goog.bind
not this = AngularJS::angular().getAMethodCall() and
not getReceiver().accessesGlobal("goog")
}
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
index >= 0 and
callback = getReceiver() and
argument = getArgument(index + 1)
}
override DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) {
callback = getReceiver() and
boundArgs = getNumArgument() - 1 and
result = this
}
override DataFlow::Node getBoundReceiver() {
result = getArgument(0)
}
}
/**
* A partial call through `_.partial`.
*/
private class LodashPartialCall extends PartialInvokeNode::Range, DataFlow::CallNode {
LodashPartialCall() { this = LodashUnderscore::member("partial").getACall() }
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
index >= 0 and
callback = getArgument(0) and
argument = getArgument(index + 1)
}
override DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) {
callback = getArgument(0) and
boundArgs = getNumArgument() - 1 and
result = this
}
}
/**
* A partial call through `ramda.partial`.
*/
private class RamdaPartialCall extends PartialInvokeNode::Range, DataFlow::CallNode {
RamdaPartialCall() { this = DataFlow::moduleMember("ramda", "partial").getACall() }
private DataFlow::ArrayCreationNode getArgumentsArray() {
result.flowsTo(getArgument(1))
}
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
callback = getArgument(0) and
argument = getArgumentsArray().getElement(index)
}
override DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) {
callback = getArgument(0) and
boundArgs = getArgumentsArray().getSize() and
result = this
}
}
}
/**
* DEPRECATED. Subclasses should extend `PartialInvokeNode::Range` instead,
* and predicates should use `PartialInvokeNode` instead.
*
* An invocation that is modeled as a partial function application.
*
* This contributes additional argument-passing flow edges that should be added to all data flow configurations.
*/
deprecated class AdditionalPartialInvokeNode = PartialInvokeNode::Range;

View File

@@ -235,7 +235,8 @@ module SourceNode {
astNode instanceof FunctionSentExpr or
astNode instanceof FunctionBindExpr or
astNode instanceof DynamicImportExpr or
astNode instanceof ImportSpecifier
astNode instanceof ImportSpecifier or
astNode instanceof ImportMetaExpr
)
or
DataFlow::parameterNode(this, _)

View File

@@ -55,6 +55,7 @@ module StepSummary {
/**
* INTERNAL: Use `SourceNode.track()` or `SourceNode.backtrack()` instead.
*/
cached
predicate step(DataFlow::SourceNode pred, DataFlow::SourceNode succ, StepSummary summary) {
exists(DataFlow::Node mid | pred.flowsTo(mid) | smallstep(mid, succ, summary))
}
@@ -170,6 +171,7 @@ class TypeTracker extends TTypeTracker {
TypeTracker() { this = MkTypeTracker(hasCall, prop) }
/** Gets the summary resulting from appending `step` to this type-tracking summary. */
cached
TypeTracker append(StepSummary step) {
step = LevelStep() and result = this
or
@@ -388,7 +390,7 @@ class TypeBackTracker extends TTypeBackTracker {
* t.start() and
* result = < some API call >.getArgument(< n >)
* or
* exists (DataFlow::TypeTracker t2 |
* exists (DataFlow::TypeBackTracker t2 |
* t = t2.smallstep(result, myType(t2))
* )
* }

View File

@@ -0,0 +1,149 @@
/**
* Internal predicates for computing the call graph.
*/
private import javascript
cached
module CallGraph {
/** Gets the function referenced by `node`, as determined by the type inference. */
cached
Function getAFunctionValue(AnalyzedNode node) {
result = node.getAValue().(AbstractCallable).getFunction()
}
/** Holds if the type inferred for `node` is indefinite due to global flow. */
cached
predicate isIndefiniteGlobal(AnalyzedNode node) {
node.analyze().getAValue().isIndefinite("global")
}
/**
* Gets a data flow node that refers to the given function.
*
* Note that functions are not currently type-tracked, but this exposes the type-tracker `t`
* from underlying class tracking if the function came from a class or instance.
*/
pragma[nomagic]
private
DataFlow::SourceNode getAFunctionReference(DataFlow::FunctionNode function, int imprecision, DataFlow::TypeTracker t) {
t.start() and
exists(Function fun |
fun = function.getFunction() and
fun = getAFunctionValue(result)
|
if isIndefiniteGlobal(result) then
fun.getFile() = result.getFile() and imprecision = 0
or
fun.inExternsFile() and imprecision = 1
or
imprecision = 2
else
imprecision = 0
)
or
imprecision = 0 and
t.start() and
exists(string name |
GlobalAccessPath::isAssignedInUniqueFile(name) and
GlobalAccessPath::fromRhs(function) = name and
GlobalAccessPath::fromReference(result) = name
)
or
imprecision = 0 and
exists(DataFlow::ClassNode cls |
exists(string name |
function = cls.getInstanceMethod(name) and
getAnInstanceMemberAccess(cls, name, t.continue()).flowsTo(result)
or
function = cls.getStaticMethod(name) and
cls.getAClassReference(t.continue()).getAPropertyRead(name).flowsTo(result)
)
or
function = cls.getConstructor() and
cls.getAClassReference(t.continue()).flowsTo(result)
)
}
/**
* Gets a data flow node that refers to the given function.
*/
cached
DataFlow::SourceNode getAFunctionReference(DataFlow::FunctionNode function, int imprecision) {
result = getAFunctionReference(function, imprecision, DataFlow::TypeTracker::end())
}
/**
* Gets a data flow node that refers to the result of the given partial function invocation,
* with `function` as the underlying function.
*/
pragma[nomagic]
private
DataFlow::SourceNode getABoundFunctionReference(DataFlow::FunctionNode function, int boundArgs, DataFlow::TypeTracker t) {
exists(DataFlow::PartialInvokeNode partial, DataFlow::Node callback |
result = partial.getBoundFunction(callback, boundArgs) and
getAFunctionReference(function, 0, t.continue()).flowsTo(callback)
)
or
exists(DataFlow::StepSummary summary, DataFlow::TypeTracker t2 |
result = getABoundFunctionReferenceAux(function, boundArgs, t2, summary) and
t = t2.append(summary)
)
}
pragma[noinline]
private
DataFlow::SourceNode getABoundFunctionReferenceAux(DataFlow::FunctionNode function, int boundArgs, DataFlow::TypeTracker t, DataFlow::StepSummary summary) {
exists(DataFlow::SourceNode prev |
prev = getABoundFunctionReference(function, boundArgs, t) and
DataFlow::StepSummary::step(prev, result, summary)
)
}
/**
* Gets a data flow node that refers to the result of the given partial function invocation,
* with `function` as the underlying function.
*/
cached
DataFlow::SourceNode getABoundFunctionReference(DataFlow::FunctionNode function, int boundArgs) {
result = getABoundFunctionReference(function, boundArgs, DataFlow::TypeTracker::end())
}
/**
* Gets a property read that accesses the property `name` on an instance of this class.
*
* Concretely, this holds when the base is an instance of this class or a subclass thereof.
*
* This predicate may be overridden to customize the class hierarchy analysis.
*/
pragma[nomagic]
private
DataFlow::PropRead getAnInstanceMemberAccess(DataFlow::ClassNode cls, string name, DataFlow::TypeTracker t) {
result = cls.getAnInstanceReference(t.continue()).getAPropertyRead(name)
or
exists(DataFlow::ClassNode subclass |
result = getAnInstanceMemberAccess(subclass, name, t) and
not exists(subclass.getInstanceMember(name, _)) and
cls = subclass.getADirectSuperClass()
)
}
/**
* Gets a possible callee of `node` with the given `imprecision`.
*
* Does not include custom call edges.
*/
cached
DataFlow::FunctionNode getACallee(DataFlow::InvokeNode node, int imprecision) {
getAFunctionReference(result, imprecision).flowsTo(node.getCalleeNode())
or
imprecision = 0 and
exists(InvokeExpr expr | expr = node.(DataFlow::Impl::ExplicitInvokeNode).asExpr() |
result.getFunction() = expr.getResolvedCallee()
or
exists(DataFlow::ClassNode cls |
expr.(SuperCall).getBinder() = cls.getConstructor().getFunction() and
result = cls.getADirectSuperClass().getConstructor()
)
)
}
}

View File

@@ -97,54 +97,11 @@ private module CachedSteps {
f = cap.getContainer()
}
/**
* Holds if the method invoked by `invoke` resolved to a member named `name` in `cls`
* or one of its super classes.
*/
cached
predicate callResolvesToMember(DataFlow::InvokeNode invoke, DataFlow::ClassNode cls, string name) {
invoke = cls.getAnInstanceReference().getAMethodCall(name)
or
exists(DataFlow::ClassNode subclass |
callResolvesToMember(invoke, subclass, name) and
not exists(subclass.getAnInstanceMember(name)) and
cls = subclass.getADirectSuperClass()
)
}
/**
* Holds if `invk` may invoke `f`.
*/
cached
predicate calls(DataFlow::InvokeNode invk, Function f) {
f = invk.getACallee(0)
or
exists(DataFlow::ClassNode cls |
// Call to class member
exists(string name |
callResolvesToMember(invk, cls, name) and
f = cls.getInstanceMethod(name).getFunction()
or
invk = cls.getAClassReference().getAMethodCall(name) and
f = cls.getStaticMethod(name).getFunction()
)
or
// Call to constructor
invk = cls.getAClassReference().getAnInvocation() and
f = cls.getConstructor().getFunction()
or
// Super call to constructor
invk.asExpr().(SuperCall).getBinder() = cls.getConstructor().getFunction() and
f = cls.getADirectSuperClass().getConstructor().getFunction()
)
or
// Call from `foo.bar.baz()` to `foo.bar.baz = function()`
exists(string name |
GlobalAccessPath::isAssignedInUniqueFile(name) and
GlobalAccessPath::fromRhs(f.flow()) = name and
GlobalAccessPath::fromReference(invk.getCalleeNode()) = name
)
}
predicate calls(DataFlow::InvokeNode invk, Function f) { f = invk.getACallee(0) }
/**
* Holds if `invk` may invoke `f` indirectly through the given `callback` argument.
@@ -152,7 +109,7 @@ private module CachedSteps {
* This only holds for explicitly modeled partial calls.
*/
private predicate partiallyCalls(
DataFlow::AdditionalPartialInvokeNode invk, DataFlow::AnalyzedNode callback, Function f
DataFlow::PartialInvokeNode invk, DataFlow::AnalyzedNode callback, Function f
) {
invk.isPartialArgument(callback, _, _) and
exists(AbstractFunction callee | callee = callback.getAValue() |
@@ -184,7 +141,7 @@ private module CachedSteps {
)
or
exists(DataFlow::Node callback, int i, Parameter p |
invk.(DataFlow::AdditionalPartialInvokeNode).isPartialArgument(callback, arg, i) and
invk.(DataFlow::PartialInvokeNode).isPartialArgument(callback, arg, i) and
partiallyCalls(invk, callback, f) and
f.getParameter(i) = p and
not p.isRestParameter() and
@@ -525,3 +482,4 @@ module PathSummary {
*/
PathSummary return() { exists(FlowLabel lbl | result = MkPathSummary(true, false, lbl, lbl)) }
}

View File

@@ -50,17 +50,17 @@ abstract class AnalyzedPropertyRead extends DataFlow::AnalyzedNode {
}
/**
* Flow analysis for (non-numeric) property read accesses.
* Flow analysis for non-numeric property read accesses.
*/
private class AnalyzedPropertyAccess extends AnalyzedPropertyRead, DataFlow::ValueNode {
override PropAccess astNode;
private class AnalyzedNonNumericPropertyRead extends AnalyzedPropertyRead {
DataFlow::PropRead self;
DataFlow::AnalyzedNode baseNode;
string propName;
AnalyzedPropertyAccess() {
astNode.accesses(baseNode.asExpr(), propName) and
isNonNumericPropertyName(propName) and
astNode instanceof RValue
AnalyzedNonNumericPropertyRead() {
this = self and
self.accesses(baseNode, propName) and
isNonNumericPropertyName(propName)
}
override predicate reads(AbstractValue base, string prop) {
@@ -148,7 +148,7 @@ private predicate explicitPropertyWrite(
* Flow analysis for `arguments.callee`. We assume it is never redefined,
* which is unsound in practice, but pragmatically useful.
*/
private class AnalyzedArgumentsCallee extends AnalyzedPropertyAccess {
private class AnalyzedArgumentsCallee extends AnalyzedNonNumericPropertyRead {
AnalyzedArgumentsCallee() { propName = "callee" }
override AbstractValue getALocalValue() {
@@ -156,18 +156,18 @@ private class AnalyzedArgumentsCallee extends AnalyzedPropertyAccess {
result = TAbstractFunction(baseVal.getFunction())
)
or
hasNonArgumentsBase(astNode) and result = super.getALocalValue()
hasNonArgumentsBase(self) and result = super.getALocalValue()
}
}
/**
* Holds if `pacc` is of the form `e.callee` where `e` could evaluate to some
* Holds if `pr` is of the form `e.callee` where `e` could evaluate to some
* value that is not an arguments object.
*/
private predicate hasNonArgumentsBase(PropAccess pacc) {
pacc.getPropertyName() = "callee" and
private predicate hasNonArgumentsBase(DataFlow::PropRead pr) {
pr.getPropertyName() = "callee" and
exists(AbstractValue baseVal |
baseVal = pacc.getBase().analyze().getALocalValue() and
baseVal = pr.getBase().analyze().getALocalValue() and
not baseVal instanceof AbstractArguments
)
}

View File

@@ -223,7 +223,11 @@ abstract class AnalyzedSsaDefinition extends SsaDefinition {
* Flow analysis for SSA definitions corresponding to `VarDef`s.
*/
private class AnalyzedExplicitDefinition extends AnalyzedSsaDefinition, SsaExplicitDefinition {
override AbstractValue getAnRhsValue() { result = getDef().(AnalyzedVarDef).getAnAssignedValue() }
override AbstractValue getAnRhsValue() {
result = getDef().(AnalyzedVarDef).getAnAssignedValue()
or
result = getRhsNode().analyze().getALocalValue()
}
}
/**
@@ -241,6 +245,8 @@ private class AnalyzedVariableCapture extends AnalyzedSsaDefinition, SsaVariable
exists(LocalVariable v | v = getSourceVariable() |
result = v.(AnalyzedCapturedVariable).getALocalValue()
or
result = any(AnalyzedExplicitDefinition def | def.getSourceVariable() = v).getAnRhsValue()
or
not guaranteedToBeInitialized(v) and result = getImplicitInitValue(v)
)
}

View File

@@ -1060,21 +1060,42 @@ private class RouteInstantiatedController extends Controller {
/**
* Dataflow for the arguments of AngularJS dependency-injected functions.
*/
private class DependencyInjectedArgumentInitializer extends DataFlow::AnalyzedValueNode {
private class DependencyInjectedArgumentInitializer extends DataFlow::AnalyzedNode {
DataFlow::AnalyzedNode service;
DependencyInjectedArgumentInitializer() {
exists(
AngularJS::InjectableFunction f, SimpleParameter param, AngularJS::CustomServiceDefinition def
AngularJS::InjectableFunction f, Parameter param, AngularJS::CustomServiceDefinition def
|
astNode = param.getAnInitialUse() and
this = DataFlow::parameterNode(param) and
def.getServiceReference() = f.getAResolvedDependency(param) and
service = def.getAService()
)
}
override AbstractValue getAValue() {
result = DataFlow::AnalyzedValueNode.super.getAValue() or
result = DataFlow::AnalyzedNode.super.getAValue() or
result = service.getALocalValue()
}
}
/**
* A call to `angular.bind`, as a partial function invocation.
*/
private class BindCall extends DataFlow::PartialInvokeNode::Range, DataFlow::CallNode {
BindCall() { this = angular().getAMemberCall("bind") }
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
index >= 0 and
callback = getArgument(1) and
argument = getArgument(index + 2)
}
override DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) {
callback = getArgument(1) and
boundArgs = getNumArgument() - 2 and
result = this
}
override DataFlow::Node getBoundReceiver() { result = getArgument(0) }
}

View File

@@ -70,7 +70,7 @@ module AsyncPackage {
* to the first parameter of the final callback, while `result1, result2, ...` are propagated to
* the parameters of the following task.
*/
private class WaterfallNextTaskCall extends DataFlow::AdditionalPartialInvokeNode {
private class WaterfallNextTaskCall extends DataFlow::PartialInvokeNode::Range, DataFlow::CallNode {
Waterfall waterfall;
int n;

View File

@@ -839,6 +839,10 @@ module Express {
override DataFlow::Node getRootPathArgument() {
result = this.(DataFlow::CallNode).getOptionArgument(1, "root")
}
override predicate isUpwardNavigationRejected(DataFlow::Node argument) {
argument = getAPathArgument()
}
}
/**

View File

@@ -573,21 +573,32 @@ module NodeJSLib {
this = DataFlow::moduleMember("child_process", methodName).getACall()
}
override DataFlow::Node getACommandArgument() {
private DataFlow::Node getACommandArgument(boolean shell) {
// check whether this is an invocation of an exec/spawn/fork method
(
methodName = "exec" or
methodName = "execSync" or
methodName = "execFile" or
methodName = "execFileSync" or
methodName = "spawn" or
methodName = "spawnSync" or
methodName = "fork"
shell = true and
(
methodName = "exec" or
methodName = "execSync"
)
or
shell = false and
(
methodName = "execFile" or
methodName = "execFileSync" or
methodName = "spawn" or
methodName = "spawnSync" or
methodName = "fork"
)
) and
// all of the above methods take the command as their first argument
result = getArgument(0)
}
override DataFlow::Node getACommandArgument() { result = getACommandArgument(_) }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument(true) }
override DataFlow::Node getArgumentList() {
(
methodName = "execFile" or

View File

@@ -158,6 +158,8 @@ module ShellJS {
ShellJSExec() { name = "exec" }
override DataFlow::Node getACommandArgument() { result = getArgument(0) }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
}
/**

View File

@@ -8,18 +8,26 @@ import javascript
private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode {
int cmdArg;
boolean shell;
SystemCommandExecutors() {
exists(string mod, DataFlow::SourceNode callee |
exists(string method |
mod = "cross-spawn" and method = "sync" and cmdArg = 0
mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false
or
mod = "execa" and
(
method = "shell" or
method = "shellSync" or
method = "stdout" or
method = "stderr" or
method = "sync"
shell = false and
(
method = "shell" or
method = "shellSync" or
method = "stdout" or
method = "stderr" or
method = "sync"
)
or
shell = true and
(method = "command" or method = "commandSync")
) and
cmdArg = 0
|
@@ -27,17 +35,24 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
)
or
(
mod = "cross-spawn" and cmdArg = 0
shell = false and
(
mod = "cross-spawn" and cmdArg = 0
or
mod = "cross-spawn-async" and cmdArg = 0
or
mod = "exec-async" and cmdArg = 0
or
mod = "execa" and cmdArg = 0
)
or
mod = "cross-spawn-async" and cmdArg = 0
or
mod = "exec" and cmdArg = 0
or
mod = "exec-async" and cmdArg = 0
or
mod = "execa" and cmdArg = 0
or
mod = "remote-exec" and cmdArg = 1
shell = true and
(
mod = "exec" and
cmdArg = 0
or
mod = "remote-exec" and cmdArg = 1
)
) and
callee = DataFlow::moduleImport(mod)
|
@@ -46,4 +61,8 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
}
override DataFlow::Node getACommandArgument() { result = getArgument(cmdArg) }
override predicate isShellInterpreted(DataFlow::Node arg) {
arg = getACommandArgument() and shell = true
}
}

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* sensitive information in broken or weak cryptographic algorithms,
* as well as extension points for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* clear-text logging of sensitive information, as well as extension
* points for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* cleartext storage of sensitive information, as well as extension
* points for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* unvalidated URL redirection problems on the client side, as well as
* extension points for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* code injection vulnerabilities, as well as extension points for
* adding your own.
*/

View File

@@ -36,43 +36,4 @@ module CommandInjection {
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
}
/**
* Auxiliary data flow configuration for tracking string literals that look like they
* may refer to an operating system shell, and array literals that may end up being
* interpreted as argument lists for system commands.
*/
class ArgumentListTracking extends DataFlow::Configuration {
ArgumentListTracking() { this = "ArgumentListTracking" }
override predicate isSource(DataFlow::Node nd) {
nd instanceof DataFlow::ArrayCreationNode
or
exists(ConstantString shell | shellCmd(shell, _) | nd = DataFlow::valueNode(shell))
}
override predicate isSink(DataFlow::Node nd) {
exists(SystemCommandExecution sys |
nd = sys.getACommandArgument() or
nd = sys.getArgumentList()
)
}
}
/**
* Holds if `shell arg <cmd>` runs `<cmd>` as a shell command.
*
* That is, either `shell` is a Unix shell (`sh` or similar) and
* `arg` is `"-c"`, or `shell` is `cmd.exe` and `arg` is `"/c"`.
*/
private predicate shellCmd(ConstantString shell, string arg) {
exists(string s | s = shell.getStringValue() |
(s = "sh" or s = "bash" or s = "/bin/sh" or s = "/bin/bash") and
arg = "-c"
)
or
exists(string s | s = shell.getStringValue().toLowerCase() |
(s = "cmd" or s = "cmd.exe") and
(arg = "/c" or arg = "/C")
)
}
}

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* command-injection vulnerabilities, as well as extension points for
* adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* bypass of sensitive action guards, as well as extension points for
* adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* CORS misconfiguration for credentials transfer, as well as
* extension points for adding your own.
*/

View File

@@ -174,18 +174,20 @@ private module PersistentWebStorage {
* An event handler that handles `postMessage` events.
*/
class PostMessageEventHandler extends Function {
int paramIndex;
PostMessageEventHandler() {
exists(CallExpr addEventListener |
addEventListener.getCallee().accessesGlobal("addEventListener") and
exists(DataFlow::CallNode addEventListener |
addEventListener = DataFlow::globalVarRef("addEventListener").getACall() and
addEventListener.getArgument(0).mayHaveStringValue("message") and
addEventListener.getArgument(1).analyze().getAValue().(AbstractFunction).getFunction() = this
addEventListener.getArgument(1).getABoundFunctionValue(paramIndex).getFunction() = this
)
}
/**
* Gets the parameter that contains the event.
*/
SimpleParameter getEventParameter() { result = getParameter(0) }
Parameter getEventParameter() { result = getParameter(paramIndex) }
}
/**

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* comparisons that relies on different kinds of HTTP request data, as
* well as extension points for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* file data in outbound network requests, as well as extension points
* for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* hardcoded credentials, as well as extension points for adding your
* own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* hard-coded data being interpreted as code, as well as extension
* points for adding your own.
*/

View File

@@ -1,5 +1,5 @@
/**
* Provides default sources, sinks and sanitisers for reasoning about
* Provides default sources, sinks and sanitizers for reasoning about
* writing user-controlled data to files, as well as extension points
* for adding your own.
*/

Some files were not shown because too many files have changed in this diff Show More