mirror of
https://github.com/github/codeql.git
synced 2026-04-24 16:25:15 +02:00
Merge remote-tracking branch 'upstream/master' into deferredModel
This commit is contained in:
@@ -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>
|
||||
@@ -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 + "."
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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>
|
||||
140
javascript/ql/src/Declarations/UnreachableMethodOverloads.ql
Normal file
140
javascript/ql/src/Declarations/UnreachableMethodOverloads.ql
Normal 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"
|
||||
@@ -0,0 +1,5 @@
|
||||
interface Foo {
|
||||
getParsedThing(id: string): string[];
|
||||
getParsedThing(id: string): number[];
|
||||
getParsedThing(id: string): object[];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
interface Foo {
|
||||
getParsedThing(id: string): object[] | number[] | string[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
interface Foo {
|
||||
create<T>(a: string): MyObject<T>;
|
||||
create(a: string): MyObject<any>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
interface Foo {
|
||||
create(a: string): Array<any>;
|
||||
create<T>(a: string): Array<T>;
|
||||
}
|
||||
@@ -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 > 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>
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 + "'."
|
||||
@@ -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 + "'."
|
||||
|
||||
@@ -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)") + "'."
|
||||
|
||||
@@ -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>
|
||||
@@ -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() + "'."
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
@@ -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>
|
||||
@@ -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() + "."
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -0,0 +1,2 @@
|
||||
let regex = new RegExp('(^\s*)my-marker(\s*$)'),
|
||||
isMyMarkerText = regex.test(text);
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>&lt;</code>
|
||||
and the double-quote character as <code>&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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @id js/loop-bound-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-834
|
||||
* @precision medium
|
||||
* @precision high
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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."
|
||||
43
javascript/ql/src/Statements/UseOfReturnlessFunction.qhelp
Normal file
43
javascript/ql/src/Statements/UseOfReturnlessFunction.qhelp
Normal 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>
|
||||
168
javascript/ql/src/Statements/UseOfReturnlessFunction.ql
Normal file
168
javascript/ql/src/Statements/UseOfReturnlessFunction.ql
Normal 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
|
||||
@@ -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.
|
||||
|
||||
21
javascript/ql/src/Statements/UselessConditional.qll
Normal file
21
javascript/ql/src/Statements/UselessConditional.qll
Normal 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()
|
||||
}
|
||||
@@ -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);
|
||||
4
javascript/ql/src/codeql-suites/javascript-lgtm-full.qls
Normal file
4
javascript/ql/src/codeql-suites/javascript-lgtm-full.qls
Normal 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
|
||||
4
javascript/ql/src/codeql-suites/javascript-lgtm.qls
Normal file
4
javascript/ql/src/codeql-suites/javascript-lgtm.qls
Normal 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
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
4
javascript/ql/src/qlpack.yml
Normal file
4
javascript/ql/src/qlpack.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: codeql-javascript
|
||||
version: 0.0.0
|
||||
dbscheme: semmlecode.javascript.dbscheme
|
||||
suites: codeql-suites
|
||||
97
javascript/ql/src/semmle/javascript/CharacterEscapes.qll
Normal file
97
javascript/ql/src/semmle/javascript/CharacterEscapes.qll
Normal 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -641,4 +641,4 @@ class OriginalExportDeclaration extends ExportDeclaration {
|
||||
result = this.(ExportDefaultDeclaration).getSourceNode(name) or
|
||||
result = this.(ExportNamedDeclaration).getSourceNode(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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, _)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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` → `nd1` and `nd1` → `nd2`,
|
||||
* to which `succ1` will add the edge `src` → `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` → `nd2` and `nd2` → `snk`,
|
||||
* while `succ2` will yield just one edge `nd1` → `snk`.
|
||||
*/
|
||||
private PathNode succ2(PathNode nd) {
|
||||
result = succ1(nd)
|
||||
or
|
||||
succ1(nd) = finalMidNode(result)
|
||||
}
|
||||
|
||||
/** Holds if `pred` → `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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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, _)
|
||||
|
||||
@@ -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))
|
||||
* )
|
||||
* }
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)) }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -158,6 +158,8 @@ module ShellJS {
|
||||
ShellJSExec() { name = "exec" }
|
||||
|
||||
override DataFlow::Node getACommandArgument() { result = getArgument(0) }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user