Files
codeql/javascript/ql/lib/semmle/javascript/StandardLibrary.qll

243 lines
8.2 KiB
Plaintext

/** Provides classes for working with standard library objects. */
import javascript
/**
* A call to `Object.defineProperty`.
*/
class CallToObjectDefineProperty extends DataFlow::MethodCallNode {
CallToObjectDefineProperty() {
exists(GlobalVariable obj |
obj.getName() = "Object" and
this.calls(DataFlow::valueNode(obj.getAnAccess()), "defineProperty")
)
}
/** Gets the data flow node denoting the object on which the property is defined. */
DataFlow::Node getBaseObject() { result = this.getArgument(0) }
/** Gets the name of the property being defined, if it can be determined. */
string getPropertyName() { result = this.getArgument(1).getStringValue() }
/** Gets the data flow node denoting the descriptor of the property being defined. */
DataFlow::Node getPropertyDescriptor() { result = this.getArgument(2) }
/**
* Holds if there is an assignment to property `name` to the
* attributes object on this node, and the right hand side of the
* assignment is `rhs`.
*/
predicate hasPropertyAttributeWrite(string name, DataFlow::Node rhs) {
exists(DataFlow::SourceNode descriptor |
descriptor.flowsTo(this.getPropertyDescriptor()) and
descriptor.hasPropertyWrite(name, rhs)
)
}
}
/**
* A direct call to `eval`.
*/
class DirectEval extends CallExpr {
DirectEval() { this.getCallee().(GlobalVarAccess).getName() = "eval" }
/** Holds if this call could affect the value of `lv`. */
predicate mayAffect(LocalVariable lv) { this.getParent+() = lv.getScope().getScopeElement() }
}
/**
* Models `Array.prototype.map` and friends as partial invocations that pass their second
* argument as the receiver to the callback.
*/
private class ArrayIterationCallbackAsPartialInvoke extends DataFlow::PartialInvokeNode::Range,
DataFlow::MethodCallNode
{
ArrayIterationCallbackAsPartialInvoke() {
this.getNumArgument() = 2 and
// Filter out library methods named 'forEach' etc
not DataFlow::moduleImport(_).flowsTo(this.getReceiver()) and
this.getMethodName() = ["filter", "forEach", "map", "some", "every"]
}
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
callback = this.getArgument(0) and
result = this.getArgument(1)
}
}
/**
* A flow step propagating the exception thrown from a callback to a method whose name coincides
* a built-in Array iteration method, such as `forEach` or `map`.
*/
private class IteratorExceptionStep extends DataFlow::LegacyFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::MethodCallNode call |
call.getMethodName() = ["forEach", "each", "map", "filter", "some", "every", "fold", "reduce"] and
pred = call.getAnArgument().(DataFlow::FunctionNode).getExceptionalReturn() and
succ = call.getExceptionalReturn()
)
}
}
/**
* A call to `String.prototype.replace`.
*
* We heuristically include any call to a method called `replace`, provided it either
* has exactly two arguments, or local data flow suggests that the receiver may be a string.
*/
class StringReplaceCall extends DataFlow::MethodCallNode {
StringReplaceCall() {
this.getMethodName() = ["replace", "replaceAll"] and
(this.getNumArgument() = 2 or this.getReceiver().mayHaveStringValue(_))
}
/** Gets the regular expression passed as the first argument to `replace`, if any. */
DataFlow::RegExpCreationNode getRegExp() { result.flowsTo(this.getArgument(0)) }
/** Gets a string that is being replaced by this call. */
string getAReplacedString() {
result = this.getRegExp().getRoot().getAMatchedString() or
this.getArgument(0).mayHaveStringValue(result)
}
/**
* Gets the second argument of this call to `replace`, which is either a string
* or a callback.
*/
DataFlow::Node getRawReplacement() { result = this.getArgument(1) }
/**
* Gets a function flowing into the second argument of this call to `replace`.
*/
DataFlow::FunctionNode getReplacementCallback() { result = this.getCallback(1) }
/**
* Holds if this is a global replacement, that is, the first argument is a regular expression
* with the `g` flag, or this is a call to `.replaceAll()`.
*/
predicate isGlobal() { this.getRegExp().isGlobal() or this.getMethodName() = "replaceAll" }
/**
* Holds if this is a global replacement, that is, the first argument is a regular expression
* with the `g` flag or unknown flags, or this is a call to `.replaceAll()`.
*/
predicate maybeGlobal() { this.getRegExp().maybeGlobal() or this.getMethodName() = "replaceAll" }
/**
* Holds if this call to `replace` replaces `old` with `new`.
*/
predicate replaces(string old, string new) {
exists(string rawNew |
old = this.getAReplacedString() and
this.getRawReplacement().mayHaveStringValue(rawNew) and
new = rawNew.replaceAll("$&", old)
)
or
exists(DataFlow::FunctionNode replacer, DataFlow::PropRead pr, DataFlow::ObjectLiteralNode map |
replacer = this.getCallback(1) and
replacer.getParameter(0).flowsToExpr(pr.getPropertyNameExpr()) and
pr = map.getAPropertyRead() and
pr.flowsTo(replacer.getAReturn()) and
map.hasPropertyWrite(old, any(DataFlow::Node repl | repl.getStringValue() = new))
)
or
// str.replace(regex, match => {
// if (match === 'old') return 'new';
// if (match === 'foo') return 'bar';
// ...
// })
exists(
DataFlow::FunctionNode replacer, ConditionGuardNode guard, EqualityTest test,
DataFlow::Node ret
|
replacer = this.getCallback(1) and
guard.getOutcome() = test.getPolarity() and
guard.getTest() = test and
replacer.getParameter(0).flowsToExpr(test.getAnOperand()) and
test.getAnOperand().getStringValue() = old and
ret = replacer.getAReturn() and
guard.dominates(ret.getBasicBlock()) and
new = ret.getStringValue()
)
}
/**
* Holds if this call takes a regexp containing a wildcard-like term such as `.`.
*
* Also see `RegExp::isWildcardLike`.
*/
final predicate hasRegExpContainingWildcard() {
RegExp::isWildcardLike(this.getRegExp().getRoot().getAChild*())
}
}
/**
* A call to `String.prototype.split`.
*
* We heuristically include any call to a method called `split`, provided it either
* has one or two arguments, or local data flow suggests that the receiver may be a string.
*/
class StringSplitCall extends DataFlow::MethodCallNode {
StringSplitCall() {
this.getMethodName() = "split" and
(this.getNumArgument() = [1, 2] or this.getReceiver().mayHaveStringValue(_))
}
/**
* Gets a string that determines where the string is split.
*/
string getSeparator() {
this.getArgument(0).mayHaveStringValue(result)
or
result =
this.getArgument(0)
.getALocalSource()
.(DataFlow::RegExpCreationNode)
.getRoot()
.getAMatchedString()
}
/**
* Gets the DataFlow::Node for the base string that is split.
*/
DataFlow::Node getBaseString() { result = this.getReceiver() }
/**
* Gets a read of the `i`th element from the split string.
*/
bindingset[i]
DataFlow::Node getASubstringRead(int i) { result = this.getAPropertyRead(i.toString()) }
}
/**
* A call to `Object.prototype.hasOwnProperty`, `Object.hasOwn`, or a library that implements
* the same functionality.
*/
class HasOwnPropertyCall extends DataFlow::Node instanceof DataFlow::CallNode {
DataFlow::Node object;
DataFlow::Node property;
HasOwnPropertyCall() {
// Make sure we handle reflective calls since libraries love to do that.
super.getCalleeNode().getALocalSource().(DataFlow::PropRead).getPropertyName() =
"hasOwnProperty" and
object = super.getReceiver() and
property = super.getArgument(0)
or
this =
[
DataFlow::globalVarRef("Object").getAMemberCall("hasOwn"), //
DataFlow::moduleImport("has").getACall(), //
LodashUnderscore::member("has").getACall()
] and
object = super.getArgument(0) and
property = super.getArgument(1)
}
/** Gets the object whose property is being checked. */
DataFlow::Node getObject() { result = object }
/** Gets the property being checked. */
DataFlow::Node getProperty() { result = property }
}