Files
codeql/javascript/ql/lib/semmle/javascript/DefensiveProgramming.qll
2022-12-12 16:06:57 +01:00

395 lines
12 KiB
Plaintext

/**
* Provides classes for working with defensive programming patterns.
*/
import javascript
private import semmle.javascript.dataflow.InferredTypes
/**
* A test in a defensive programming pattern.
*/
abstract class DefensiveExpressionTest extends DataFlow::ValueNode {
/** Gets the unique Boolean value that this test evaluates to, if any. */
abstract boolean getTheTestResult();
}
/**
* Provides classes for specific kinds of defensive programming patterns.
*/
module DefensiveExpressionTest {
/**
* A defensive truthiness check that may be worth keeping, even if it
* is strictly speaking useless.
*
* We currently recognize three patterns:
*
* - the first `x` in `x || (x = e)`
* - the second `x` in `x = (x || e)`
* - the second `x` in `var x = x || e`
*/
class DefensiveInit extends DefensiveExpressionTest {
DefensiveInit() {
exists(VarAccess va, LogOrExpr o, VarRef va2 |
va = astNode and
va = o.getLeftOperand().stripParens() and
va2.getVariable() = va.getVariable()
|
exists(AssignExpr assgn | va2 = assgn.getTarget() |
assgn = o.getRightOperand().stripParens() or
o = assgn.getRhs().stripParens()
)
or
exists(VariableDeclarator vd | va2 = vd.getBindingPattern() |
o = vd.getInit().stripParens()
)
)
}
override boolean getTheTestResult() { result = this.analyze().getTheBooleanValue() }
}
/**
* Gets the inner expression of `e`, with any surrounding parentheses and boolean nots removed.
* `polarity` is true iff the inner expression is nested in an even number of negations.
*/
private Expr stripNotsAndParens(Expr e, boolean polarity) {
exists(Expr inner | inner = e.stripParens() |
if inner instanceof LogNotExpr
then result = stripNotsAndParens(inner.(LogNotExpr).getOperand(), polarity.booleanNot())
else (
result = inner and polarity = true
)
)
}
/**
* An equality test for `null` and `undefined`.
*
* Examples: `e === undefined` or `typeof e !== undefined`.
*/
abstract private class UndefinedNullTest extends EqualityTest {
/** Gets the unique Boolean value that this test evaluates to, if any. */
abstract boolean getTheTestResult();
/**
* Gets the expression that is tested for being `null` or `undefined`.
*/
abstract Expr getOperand();
}
/**
* A dis- or conjunction that tests if an expression is `null` or `undefined` in either branch.
*
* Example: a branch in `x === null || x === undefined`.
*/
private class CompositeUndefinedNullTestPart extends DefensiveExpressionTest {
UndefinedNullTest test;
boolean polarity;
CompositeUndefinedNullTestPart() {
exists(
LogicalBinaryExpr composite, Variable v, Expr op, Expr opOther, UndefinedNullTest testOther
|
composite.hasOperands(op, opOther) and
this = op.flow() and
test = stripNotsAndParens(op, polarity) and
testOther = stripNotsAndParens(opOther, _) and
test.getOperand().(VarRef).getVariable() = v and
testOther.getOperand().(VarRef).getVariable() = v
)
}
override boolean getTheTestResult() {
polarity = true and result = test.getTheTestResult()
or
polarity = false and result = test.getTheTestResult().booleanNot()
}
}
/**
* A test for `undefined` or `null` in an if-statement.
*
* Example: `if (x === null) ...`.
*/
private class ConsistencyCheckingUndefinedNullGuard extends DefensiveExpressionTest {
UndefinedNullTest test;
boolean polarity;
ConsistencyCheckingUndefinedNullGuard() {
exists(IfStmt c |
this = c.getCondition().flow() and
test = stripNotsAndParens(c.getCondition(), polarity) and
test.getOperand() instanceof VarRef
)
}
override boolean getTheTestResult() {
polarity = true and result = test.getTheTestResult()
or
polarity = false and result = test.getTheTestResult().booleanNot()
}
}
/**
* Holds if `t` is `null` or `undefined`.
*/
private predicate isNullOrUndefined(InferredType t) {
t = TTNull() or
t = TTUndefined()
}
/**
* Holds if `t` is not `null` or `undefined`.
*/
private predicate isNotNullOrUndefined(InferredType t) { not isNullOrUndefined(t) }
/**
* A value comparison for `null` and `undefined`.
*
* Examples: `x === null` or `x != undefined`.
*/
private class NullUndefinedComparison extends UndefinedNullTest {
Expr operand;
InferredType op2type;
NullUndefinedComparison() {
exists(Expr op2 | this.hasOperands(operand, op2) |
op2type = TTNull() and SyntacticConstants::isNull(op2)
or
op2type = TTUndefined() and SyntacticConstants::isUndefined(op2)
)
}
override boolean getTheTestResult() {
result = this.getPolarity() and
(
if this instanceof StrictEqualityTest
then
// case: `operand === null` or `operand === undefined`
operand.analyze().getTheType() = op2type
else
// case: `operand == null` or `operand == undefined`
not isNotNullOrUndefined(operand.analyze().getAType())
)
or
result = this.getPolarity().booleanNot() and
(
if this instanceof StrictEqualityTest
then
// case: `operand !== null` or `operand !== undefined`
not operand.analyze().getAType() = op2type
else
// case: `operand != null` or `operand != undefined`
not isNullOrUndefined(operand.analyze().getAType())
)
}
override Expr getOperand() { result = operand }
}
/**
* A comparison against `undefined`, such as `x === undefined`.
*/
class UndefinedComparison extends NullUndefinedComparison {
UndefinedComparison() { op2type = TTUndefined() }
}
/**
* An expression that throws an exception if one of its subexpressions evaluates to `null` or `undefined`.
*
* Examples: `sub.p` or `sub()`.
*/
private class UndefinedNullCrashUse extends Expr {
Expr target;
UndefinedNullCrashUse() {
exists(Expr thrower | stripNotsAndParens(this, _) = thrower |
thrower.(InvokeExpr).getCallee().getUnderlyingValue() = target
or
thrower.(PropAccess).getBase().getUnderlyingValue() = target
or
thrower.(MethodCallExpr).getReceiver().getUnderlyingValue() = target
)
}
/**
* Gets the subexpression that will cause an exception to be thrown if it is `null` or `undefined`.
*/
Expr getVulnerableSubexpression() { result = target }
}
/**
* An expression that throws an exception if one of its subexpressions is not a `function`.
*
* Example: `sub()`.
*/
private class NonFunctionCallCrashUse extends Expr {
Expr target;
NonFunctionCallCrashUse() {
stripNotsAndParens(this, _).(InvokeExpr).getCallee().getUnderlyingValue() = target
}
/**
* Gets the subexpression that will cause an exception to be thrown if it is not a `function`.
*/
Expr getVulnerableSubexpression() { result = target }
}
/**
* Gets the first expression that is guarded by `guard`.
*/
private Expr getAGuardedExpr(Expr guard) {
exists(LogicalBinaryExpr op |
op.getLeftOperand() = guard and
op.getRightOperand() = result
)
or
exists(IfStmt c, ExprStmt guardedStmt |
c.getCondition() = guard and
result = guardedStmt.getExpr()
|
guardedStmt = c.getAControlledStmt() or
guardedStmt = c.getAControlledStmt().(BlockStmt).getStmt(0)
)
or
exists(ConditionalExpr c | c.getCondition() = guard | result = c.getABranch())
}
/**
* Holds if `t` is `string`, `number` or `boolean`.
*/
private predicate isStringOrNumOrBool(InferredType t) {
t = TTString() or
t = TTNumber() or
t = TTBoolean()
}
/**
* A defensive expression that tests for `undefined` and `null` using a truthiness test.
*
* Examples: The condition in `if(x) { x.p; }` or `!x || x.m()`.
*/
private class UndefinedNullTruthinessGuard extends DefensiveExpressionTest {
VarRef guardVar;
boolean polarity;
UndefinedNullTruthinessGuard() {
exists(VarRef useVar |
guardVar = stripNotsAndParens(this.asExpr(), polarity) and
guardVar.getVariable() = useVar.getVariable()
|
getAGuardedExpr(this.asExpr()).(UndefinedNullCrashUse).getVulnerableSubexpression() = useVar and
// exclude types whose truthiness depend on the value
not isStringOrNumOrBool(guardVar.analyze().getAType())
)
}
override boolean getTheTestResult() {
exists(boolean testResult | testResult = guardVar.analyze().getTheBooleanValue() |
if polarity = true then result = testResult else result = testResult.booleanNot()
)
}
}
/**
* A defensive expression that tests for `undefined` and `null`.
*
* Example: the condition in `if(x !== null) { x.p; }`.
*/
private class UndefinedNullTypeGuard extends DefensiveExpressionTest {
UndefinedNullTest test;
boolean polarity;
UndefinedNullTypeGuard() {
exists(Expr guard, VarRef guardVar, VarRef useVar |
this = guard.flow() and
test = stripNotsAndParens(guard, polarity) and
test.getOperand() = guardVar and
guardVar.getVariable() = useVar.getVariable()
|
getAGuardedExpr(guard).(UndefinedNullCrashUse).getVulnerableSubexpression() = useVar
)
}
override boolean getTheTestResult() {
polarity = true and result = test.getTheTestResult()
or
polarity = false and result = test.getTheTestResult().booleanNot()
}
}
/**
* A test for the value of a `typeof` expression.
*
* Example: `typeof x === 'undefined'`.
*/
private class TypeofTest extends EqualityTest {
Expr operand;
TypeofTag tag;
TypeofTest() { TaintTracking::isTypeofGuard(this, operand, tag) }
boolean getTheTestResult() {
exists(boolean testResult |
testResult = true and operand.analyze().getTheType().getTypeofTag() = tag
or
testResult = false and not operand.analyze().getAType().getTypeofTag() = tag
|
if this.getPolarity() = true then result = testResult else result = testResult.booleanNot()
)
}
/**
* Gets the operand used in the `typeof` expression.
*/
Expr getOperand() { result = operand }
/**
* Gets the `typeof` tag that is tested.
*/
TypeofTag getTag() { result = tag }
}
/**
* A defensive expression that tests if an expression has type `function`.
*
* Example: the condition in `if(typeof x === 'function') x()`.
*/
private class FunctionTypeGuard extends DefensiveExpressionTest {
TypeofTest test;
boolean polarity;
FunctionTypeGuard() {
exists(Expr guard, VarRef guardVar, VarRef useVar |
this = guard.flow() and
test = stripNotsAndParens(guard, polarity) and
test.getOperand() = guardVar and
guardVar.getVariable() = useVar.getVariable()
|
getAGuardedExpr(guard).(NonFunctionCallCrashUse).getVulnerableSubexpression() = useVar
) and
test.getTag() = "function"
}
override boolean getTheTestResult() {
polarity = true and result = test.getTheTestResult()
or
polarity = false and result = test.getTheTestResult().booleanNot()
}
}
/**
* A test for `undefined` using a `typeof` expression.
*
* Example: `typeof x === "undefined"'.
*/
class TypeofUndefinedTest extends UndefinedNullTest instanceof TypeofTest {
TypeofUndefinedTest() { super.getTag() = "undefined" }
override boolean getTheTestResult() { result = TypeofTest.super.getTheTestResult() }
override Expr getOperand() { result = TypeofTest.super.getOperand() }
}
}