mirror of
https://github.com/github/codeql.git
synced 2026-02-15 06:23:42 +01:00
176 lines
5.8 KiB
Plaintext
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."
|