Files
codeql/javascript/ql/src/Security/CWE-020/IncorrectSuffixCheck.ql
2022-03-16 22:56:52 +01:00

176 lines
5.8 KiB
Plaintext

/**
* @name Incorrect suffix check
* @description Using indexOf to implement endsWith functionality is error-prone if the -1 case is not explicitly handled.
* @kind problem
* @problem.severity error
* @security-severity 7.8
* @precision high
* @id js/incorrect-suffix-check
* @tags security
* correctness
* external/cwe/cwe-020
*/
import javascript
/**
* A call to `indexOf` or `lastIndexOf`.
*/
class IndexOfCall extends DataFlow::MethodCallNode {
IndexOfCall() {
exists(string name | name = this.getMethodName() |
name = "indexOf" or
name = "lastIndexOf"
) and
this.getNumArgument() = 1
}
/** Gets the receiver or argument of this call. */
DataFlow::Node getAnOperand() {
result = this.getReceiver() or
result = this.getArgument(0)
}
/**
* Holds if `recv` is the local source of the receiver of this call, and `m`
* is the name of the invoked method.
*/
private predicate receiverAndMethodName(DataFlow::Node recv, string m) {
this.getReceiver().getALocalSource() = recv and
this.getMethodName() = m
}
/**
* Gets an `indexOf` call with the same receiver, argument, and method name, including this call itself.
*/
IndexOfCall getAnEquivalentIndexOfCall() {
exists(DataFlow::Node recv, string m |
this.receiverAndMethodName(recv, m) and result.receiverAndMethodName(recv, m)
|
result.getArgument(0).getALocalSource() = this.getArgument(0).getALocalSource()
or
result.getArgument(0).getStringValue() = this.getArgument(0).getStringValue()
)
}
/**
* Gets an expression that refers to the return value of this call.
*/
Expr getAUse() { this.flowsToExpr(result) }
}
/**
* Gets a source of the given string value, or one of its operands if it is a concatenation.
*/
DataFlow::SourceNode getStringSource(DataFlow::Node node) {
result = node.getALocalSource()
or
result = StringConcatenation::getAnOperand(node).getALocalSource()
}
/**
* An expression that takes the length of a string literal.
*/
class LiteralLengthExpr extends DotExpr {
LiteralLengthExpr() {
this.getPropertyName() = "length" and
this.getBase() instanceof StringLiteral
}
/**
* Gets the value of the string literal whose length is taken.
*/
string getBaseValue() { result = this.getBase().getStringValue() }
}
/**
* Holds if `length` is derived from the length of the given indexOf `operand`.
*/
predicate isDerivedFromLength(DataFlow::Node length, DataFlow::Node operand) {
exists(IndexOfCall call | operand = call.getAnOperand() |
length = getStringSource(operand).getAPropertyRead("length")
or
exists(string val | val = operand.getStringValue() |
// Find a literal length with the same string constant
exists(LiteralLengthExpr lengthExpr |
lengthExpr.getContainer() = call.getContainer() and
lengthExpr.getBaseValue() = val and
length = lengthExpr.flow()
)
or
// Find an integer constant that equals the length of string constant
exists(Expr lengthExpr |
lengthExpr.getContainer() = call.getContainer() and
lengthExpr.getIntValue() = val.length() and
length = lengthExpr.flow()
)
)
)
or
isDerivedFromLength(length.getAPredecessor(), operand)
or
exists(BinaryExpr expr | expr instanceof SubExpr or expr instanceof AddExpr |
isDerivedFromLength(expr.getAnOperand().flow(), operand) and
length = expr.flow()
)
}
/**
* An equality comparison of form `A.indexOf(B) === A.length - B.length` or similar.
*
* We assume A and B are strings, even A and/or B could be also be arrays.
* The comparison with the length rarely occurs for arrays, however.
*/
class UnsafeIndexOfComparison extends EqualityTest {
IndexOfCall indexOf;
UnsafeIndexOfComparison() {
exists(DataFlow::Node testedValue |
this.hasOperands(indexOf.getAUse(), testedValue.asExpr()) and
isDerivedFromLength(testedValue, indexOf.getReceiver()) and
isDerivedFromLength(testedValue, indexOf.getArgument(0))
) and
// Ignore cases like `x.indexOf("/") === x.length - 1` that can only be bypassed if `x` is the empty string.
// Sometimes strings are just known to be non-empty from the context, and it is unlikely to be a security issue,
// since it's obviously not a domain name check.
not indexOf.getArgument(0).mayHaveStringValue(any(string s | s.length() = 1)) and
// Relative string length comparison, such as A.length > B.length, or (A.length - B.length) > 0
not exists(RelationalComparison compare |
isDerivedFromLength(compare.getAnOperand().flow(), indexOf.getReceiver()) and
isDerivedFromLength(compare.getAnOperand().flow(), indexOf.getArgument(0))
) and
// Check for indexOf being -1
not exists(EqualityTest test, Expr minusOne |
test.hasOperands(indexOf.getAnEquivalentIndexOfCall().getAUse(), minusOne) and
minusOne.getIntValue() = -1
) and
// Check for indexOf being >1, or >=0, etc
not exists(RelationalComparison test |
test.getGreaterOperand() = indexOf.getAnEquivalentIndexOfCall().getAUse() and
exists(int value | value = test.getLesserOperand().getIntValue() |
value >= 0
or
not test.isInclusive() and
value = -1
)
) and
// Check for indexOf being <0, or <=-1
not exists(RelationalComparison test |
test.getLesserOperand() = indexOf.getAnEquivalentIndexOfCall().getAUse() and
exists(int value | value = test.getGreaterOperand().getIntValue() |
value < 0
or
not test.isInclusive() and
value = 0
)
)
}
IndexOfCall getIndexOf() { result = indexOf }
}
from UnsafeIndexOfComparison comparison
select comparison,
"This suffix check is missing a length comparison to correctly handle " +
comparison.getIndexOf().getMethodName() + " returning -1."