mirror of
https://github.com/github/codeql.git
synced 2026-04-29 10:45:15 +02:00
Merge remote-tracking branch 'upstream/master' into ExceptionalPromise
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
+ semmlecode-javascript-queries/Security/CWE-352/MissingCsrfMiddleware.ql: /Security/CWE/CWE-352
|
||||
+ semmlecode-javascript-queries/Security/CWE-400/RemotePropertyInjection.ql: /Security/CWE/CWE-400
|
||||
+ semmlecode-javascript-queries/Security/CWE-400/PrototypePollution.ql: /Security/CWE/CWE-400
|
||||
+ semmlecode-javascript-queries/Security/CWE-400/PrototypePollutionUtility.ql: /Security/CWE/CWE-400
|
||||
+ semmlecode-javascript-queries/Security/CWE-502/UnsafeDeserialization.ql: /Security/CWE/CWE-502
|
||||
+ semmlecode-javascript-queries/Security/CWE-506/HardcodedDataInterpretedAsCode.ql: /Security/CWE/CWE-506
|
||||
+ semmlecode-javascript-queries/Security/CWE-601/ClientSideUrlRedirect.ql: /Security/CWE/CWE-601
|
||||
|
||||
@@ -156,5 +156,7 @@ predicate hasNoEffect(Expr e) {
|
||||
not exists(fe.getName())
|
||||
) and
|
||||
// exclude block-level flow type annotations. For example: `(name: empty)`.
|
||||
not e.(ParExpr).getExpression().getLastToken().getNextToken().getValue() = ":"
|
||||
not e.(ParExpr).getExpression().getLastToken().getNextToken().getValue() = ":" and
|
||||
// exclude the first statement of a try block
|
||||
not e = any(TryStmt stmt).getBody().getStmt(0).(ExprStmt).getExpr()
|
||||
}
|
||||
|
||||
@@ -32,5 +32,6 @@ where
|
||||
acc.accesses(baseNode.asExpr(), prop) and
|
||||
acc.getContainer().isStrict() and
|
||||
illegalPropAccess(baseNode.getAValue(), base, prop) and
|
||||
forex(AbstractValue av | av = baseNode.getAValue() | illegalPropAccess(av, _, prop))
|
||||
forex(AbstractValue av | av = baseNode.getAValue() | illegalPropAccess(av, _, prop)) and
|
||||
not acc = any(ExprStmt stmt).getExpr() // reported by js/useless-expression
|
||||
select acc, "Strict mode code cannot use " + base + "." + prop + "."
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Most JavaScript objects inherit the properties of the built-in <code>Object.prototype</code> object.
|
||||
Prototype pollution is a type of vulnerability in which an attacker is able to modify <code>Object.prototype</code>.
|
||||
Since most objects inherit from the compromised <code>Object.prototype</code>, the attacker can use this
|
||||
to tamper with the application logic, and often escalate to remote code execution or cross-site scripting.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
One way to cause prototype pollution is through use of an unsafe <em>merge</em> or <em>extend</em> function
|
||||
to recursively copy properties from one object to another.
|
||||
Such a function has the potential to modify any object reachable from the destination object, and
|
||||
the built-in <code>Object.prototype</code> is usually reachable through the special properties
|
||||
<code>__proto__</code> and <code>constructor.prototype</code>.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
The most effective place to guard against this is in the function that performs
|
||||
the recursive copy.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Only merge a property recursively when it is an own property of the <em>destination</em> object.
|
||||
Alternatively, blacklist the property names <code>__proto__</code> and <code>constructor</code>
|
||||
from being merged.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
This function recursively copies properties from <code>src</code> to <code>dst</code>:
|
||||
</p>
|
||||
|
||||
<sample src="examples/PrototypePollutionUtility.js"/>
|
||||
|
||||
<p>
|
||||
However, if <code>src</code> is the object <code>{"__proto__": {"isAdmin": true}}</code>,
|
||||
it will inject the property <code>isAdmin: true</code> in <code>Object.prototype</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The issue can be fixed by ensuring that only own properties of the destination object
|
||||
are merged recursively:
|
||||
</p>
|
||||
|
||||
<sample src="examples/PrototypePollutionUtility_fixed.js"/>
|
||||
|
||||
<p>
|
||||
Alternatively, blacklist the <code>__proto__</code> and <code>constructor</code> properties:
|
||||
</p>
|
||||
|
||||
<sample src="examples/PrototypePollutionUtility_fixed2.js"/>
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Prototype pollution attacks:
|
||||
<a href="https://hackerone.com/reports/380873">lodash</a>,
|
||||
<a href="https://hackerone.com/reports/454365">jQuery</a>,
|
||||
<a href="https://hackerone.com/reports/381185">extend</a>,
|
||||
<a href="https://hackerone.com/reports/430291">just-extend</a>,
|
||||
<a href="https://hackerone.com/reports/381194">merge.recursive</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
566
javascript/ql/src/Security/CWE-400/PrototypePollutionUtility.ql
Normal file
566
javascript/ql/src/Security/CWE-400/PrototypePollutionUtility.ql
Normal file
@@ -0,0 +1,566 @@
|
||||
/**
|
||||
* @name Prototype pollution in utility function
|
||||
* @description Recursively copying properties between objects may cause
|
||||
accidental modification of a built-in prototype object.
|
||||
* @kind path-problem
|
||||
* @problem.severity warning
|
||||
* @precision high
|
||||
* @id js/prototype-pollution-utility
|
||||
* @tags security
|
||||
* external/cwe/cwe-400
|
||||
* external/cwe/cwe-471
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow
|
||||
import PathGraph
|
||||
import semmle.javascript.dataflow.InferredTypes
|
||||
|
||||
/**
|
||||
* Gets a node that refers to an element of `array`, likely obtained
|
||||
* as a result of enumerating the elements of the array.
|
||||
*/
|
||||
SourceNode getAnEnumeratedArrayElement(SourceNode array) {
|
||||
exists(MethodCallNode call, string name |
|
||||
call = array.getAMethodCall(name) and
|
||||
(name = "forEach" or name = "map") and
|
||||
result = call.getCallback(0).getParameter(0)
|
||||
)
|
||||
or
|
||||
exists(DataFlow::PropRead read |
|
||||
read = array.getAPropertyRead() and
|
||||
not exists(read.getPropertyName()) and
|
||||
not read.getPropertyNameExpr().analyze().getAType() = TTString() and
|
||||
result = read
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node that refers to the name of a property obtained by enumerating
|
||||
* the properties of some object.
|
||||
*/
|
||||
abstract class EnumeratedPropName extends DataFlow::Node {
|
||||
/**
|
||||
* Gets the object whose properties are being enumerated.
|
||||
*
|
||||
* For example, gets `src` in `for (var key in src)`.
|
||||
*/
|
||||
abstract DataFlow::Node getSourceObject();
|
||||
|
||||
/**
|
||||
* Gets a local reference of the source object.
|
||||
*/
|
||||
SourceNode getASourceObjectRef() {
|
||||
exists(SourceNode root, string path |
|
||||
getSourceObject() = AccessPath::getAReferenceTo(root, path) and
|
||||
result = AccessPath::getAReferenceTo(root, path)
|
||||
)
|
||||
or
|
||||
result = getSourceObject().getALocalSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a property read that accesses the corresponding property value in the source object.
|
||||
*
|
||||
* For example, gets `src[key]` in `for (var key in src) { src[key]; }`.
|
||||
*/
|
||||
PropRead getASourceProp() {
|
||||
result = getASourceObjectRef().getAPropertyRead() and
|
||||
result.getPropertyNameExpr().flow().getImmediatePredecessor*() = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Property enumeration through `for-in` for `Object.keys` or similar.
|
||||
*/
|
||||
class ForInEnumeratedPropName extends EnumeratedPropName {
|
||||
DataFlow::Node object;
|
||||
|
||||
ForInEnumeratedPropName() {
|
||||
exists(ForInStmt stmt |
|
||||
this = DataFlow::lvalueNode(stmt.getLValue()) and
|
||||
object = stmt.getIterationDomain().flow()
|
||||
)
|
||||
or
|
||||
exists(CallNode call |
|
||||
call = globalVarRef("Object").getAMemberCall("keys")
|
||||
or
|
||||
call = globalVarRef("Object").getAMemberCall("getOwnPropertyNames")
|
||||
or
|
||||
call = globalVarRef("Reflect").getAMemberCall("ownKeys")
|
||||
|
|
||||
object = call.getArgument(0) and
|
||||
this = getAnEnumeratedArrayElement(call)
|
||||
)
|
||||
}
|
||||
|
||||
override Node getSourceObject() { result = object }
|
||||
}
|
||||
|
||||
/**
|
||||
* Property enumeration through `Object.entries`.
|
||||
*/
|
||||
class EntriesEnumeratedPropName extends EnumeratedPropName {
|
||||
CallNode entries;
|
||||
SourceNode entry;
|
||||
|
||||
EntriesEnumeratedPropName() {
|
||||
entries = globalVarRef("Object").getAMemberCall("entries") and
|
||||
entry = getAnEnumeratedArrayElement(entries) and
|
||||
this = entry.getAPropertyRead("0")
|
||||
}
|
||||
|
||||
override DataFlow::Node getSourceObject() {
|
||||
result = entries.getArgument(0)
|
||||
}
|
||||
|
||||
override PropRead getASourceProp() {
|
||||
result = super.getASourceProp()
|
||||
or
|
||||
result = entry.getAPropertyRead("1")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the properties of `node` are enumerated locally.
|
||||
*/
|
||||
predicate arePropertiesEnumerated(DataFlow::SourceNode node) {
|
||||
node = any(EnumeratedPropName name).getASourceObjectRef()
|
||||
}
|
||||
|
||||
/**
|
||||
* A dynamic property access that is not obviously an array access.
|
||||
*/
|
||||
class DynamicPropRead extends DataFlow::SourceNode, DataFlow::ValueNode {
|
||||
// Use IndexExpr instead of PropRead as we're not interested in implicit accesses like
|
||||
// rest-patterns and for-of loops.
|
||||
override IndexExpr astNode;
|
||||
|
||||
DynamicPropRead() {
|
||||
not exists(astNode.getPropertyName()) and
|
||||
// Exclude obvious array access
|
||||
astNode.getPropertyNameExpr().analyze().getAType() = TTString()
|
||||
}
|
||||
|
||||
/** Gets the base of the dynamic read. */
|
||||
DataFlow::Node getBase() { result = astNode.getBase().flow() }
|
||||
|
||||
/**
|
||||
* Holds if the value of this read was assigned to earlier in the same basic block.
|
||||
*
|
||||
* For example, this is true for `dst[x]` on line 2 below:
|
||||
* ```js
|
||||
* dst[x] = {};
|
||||
* dst[x][y] = src[y];
|
||||
* ```
|
||||
*/
|
||||
predicate hasDominatingAssignment() {
|
||||
exists(DataFlow::PropWrite write, BasicBlock bb, int i, int j, SsaVariable ssaVar |
|
||||
write = getBase().getALocalSource().getAPropertyWrite() and
|
||||
bb.getNode(i) = write.getWriteNode() and
|
||||
bb.getNode(j) = astNode and
|
||||
i < j and
|
||||
write.getPropertyNameExpr() = ssaVar.getAUse() and
|
||||
astNode.getIndex() = ssaVar.getAUse()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a dynamic property assignment of form `base[prop] = rhs`
|
||||
* which might act as the writing operation in a recursive merge function.
|
||||
*
|
||||
* Only assignments to pre-existing objects are of interest, so object/array literals
|
||||
* are not included.
|
||||
*
|
||||
* Additionally, we ignore cases where the properties of `base` are enumerated, as this
|
||||
* would typically not happen in a merge function.
|
||||
*/
|
||||
predicate dynamicPropWrite(DataFlow::Node base, DataFlow::Node prop, DataFlow::Node rhs) {
|
||||
exists(AssignExpr write, IndexExpr index |
|
||||
index = write.getLhs() and
|
||||
base = index.getBase().flow() and
|
||||
prop = index.getPropertyNameExpr().flow() and
|
||||
rhs = write.getRhs().flow() and
|
||||
not exists(prop.getStringValue()) and
|
||||
not arePropertiesEnumerated(base.getALocalSource())
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the name of a property that can lead to `Object.prototype`. */
|
||||
string unsafePropName() {
|
||||
result = "__proto__"
|
||||
or
|
||||
result = "constructor"
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow label representing an unsafe property name, or an object obtained
|
||||
* by using such a property in a dynamic read.
|
||||
*/
|
||||
class UnsafePropLabel extends FlowLabel {
|
||||
UnsafePropLabel() { this = unsafePropName() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks data from property enumerations to dynamic property writes.
|
||||
*
|
||||
* The intent is to find code of the general form:
|
||||
* ```js
|
||||
* function merge(dst, src) {
|
||||
* for (var key in src)
|
||||
* if (...)
|
||||
* merge(dst[key], src[key])
|
||||
* else
|
||||
* dst[key] = src[key]
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This configuration is used to find three separate data flow paths originating
|
||||
* from a property enumeration, all leading to the same dynamic property write.
|
||||
*
|
||||
* In particular, the base and property name of the property write should all
|
||||
* depend on the enumerated property name (`key`) and the right-hand side should
|
||||
* depend on the source property (`src[key]`), while allowing steps of form
|
||||
* `x -> x[p]` and `p -> x[p]`.
|
||||
*
|
||||
* Note that in the above example, the flow from `key` to the base of the write (`dst`)
|
||||
* requires stepping through the recursive call.
|
||||
* Such a path would be absent for a shallow copying operation, where the `dst` object
|
||||
* isn't derived from a property of the source object.
|
||||
*
|
||||
* This configuration can't enforce that all three paths must end at the same
|
||||
* dynamic property write, so we treat the paths independently here and check
|
||||
* for coinciding paths afterwards. This means this configuration can't be used as
|
||||
* a standalone configuration like in most path queries.
|
||||
*/
|
||||
class PropNameTracking extends DataFlow::Configuration {
|
||||
PropNameTracking() { this = "PropNameTracking" }
|
||||
|
||||
override predicate isSource(DataFlow::Node node, FlowLabel label) {
|
||||
label instanceof UnsafePropLabel and
|
||||
exists(EnumeratedPropName prop |
|
||||
node = prop
|
||||
or
|
||||
node = prop.getASourceProp()
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node node, FlowLabel label) {
|
||||
label instanceof UnsafePropLabel and
|
||||
(
|
||||
dynamicPropWrite(node, _, _) or
|
||||
dynamicPropWrite(_, node, _) or
|
||||
dynamicPropWrite(_, _, node)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isAdditionalFlowStep(
|
||||
DataFlow::Node pred, DataFlow::Node succ, FlowLabel predlbl, FlowLabel succlbl
|
||||
) {
|
||||
predlbl instanceof UnsafePropLabel and
|
||||
succlbl = predlbl and
|
||||
(
|
||||
// Step through `p -> x[p]`
|
||||
exists(PropRead read |
|
||||
pred = read.getPropertyNameExpr().flow() and
|
||||
not read.(DynamicPropRead).hasDominatingAssignment() and
|
||||
succ = read
|
||||
)
|
||||
or
|
||||
// Step through `x -> x[p]`
|
||||
exists(DynamicPropRead read |
|
||||
not read.hasDominatingAssignment() and
|
||||
pred = read.getBase() and
|
||||
succ = read
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isBarrier(DataFlow::Node node) {
|
||||
super.isBarrier(node)
|
||||
or
|
||||
exists(ConditionGuardNode guard, SsaRefinementNode refinement |
|
||||
node = DataFlow::ssaDefinitionNode(refinement) and
|
||||
refinement.getGuard() = guard and
|
||||
guard.getTest() instanceof VarAccess and
|
||||
guard.getOutcome() = false
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isBarrierGuard(DataFlow::BarrierGuardNode node) {
|
||||
node instanceof BlacklistEqualityGuard or
|
||||
node instanceof WhitelistEqualityGuard or
|
||||
node instanceof HasOwnPropertyGuard or
|
||||
node instanceof InExprGuard or
|
||||
node instanceof InstanceOfGuard or
|
||||
node instanceof TypeofGuard or
|
||||
node instanceof BlacklistInclusionGuard or
|
||||
node instanceof WhitelistInclusionGuard
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer guard of form `x === "__proto__"` or `x === "constructor"`.
|
||||
*/
|
||||
class BlacklistEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
|
||||
override EqualityTest astNode;
|
||||
string propName;
|
||||
|
||||
BlacklistEqualityGuard() {
|
||||
astNode.getAnOperand().getStringValue() = propName and
|
||||
propName = unsafePropName()
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, FlowLabel label) {
|
||||
e = astNode.getAnOperand() and
|
||||
outcome = astNode.getPolarity().booleanNot() and
|
||||
label = propName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An equality test with something other than `__proto__` or `constructor`.
|
||||
*/
|
||||
class WhitelistEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
|
||||
override EqualityTest astNode;
|
||||
|
||||
WhitelistEqualityGuard() {
|
||||
not astNode.getAnOperand().getStringValue() = unsafePropName() and
|
||||
astNode.getAnOperand() instanceof Literal
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, FlowLabel label) {
|
||||
e = astNode.getAnOperand() and
|
||||
outcome = astNode.getPolarity() and
|
||||
label instanceof UnsafePropLabel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer guard for calls to `Object.prototype.hasOwnProperty`.
|
||||
*
|
||||
* A malicious source object will have `__proto__` and/or `constructor` as own properties,
|
||||
* but the destination object generally doesn't. It is therefore only a sanitizer when
|
||||
* used on the destination object.
|
||||
*/
|
||||
class HasOwnPropertyGuard extends DataFlow::BarrierGuardNode, CallNode {
|
||||
HasOwnPropertyGuard() {
|
||||
// Make sure we handle reflective calls since libraries love to do that.
|
||||
getCalleeNode().getALocalSource().(DataFlow::PropRead).getPropertyName() = "hasOwnProperty" and
|
||||
exists(getReceiver()) and
|
||||
// Try to avoid `src.hasOwnProperty` by requiring that the receiver
|
||||
// does not locally have its properties enumerated. Typically there is no
|
||||
// reason to enumerate the properties of the destination object.
|
||||
not arePropertiesEnumerated(getReceiver().getALocalSource())
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e) {
|
||||
e = getArgument(0).asExpr() and outcome = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer guard for `key in dst`.
|
||||
*
|
||||
* Since `"__proto__" in obj` and `"constructor" in obj` is true for most objects,
|
||||
* this is seen as a sanitizer for `key` in the false outcome.
|
||||
*/
|
||||
class InExprGuard extends DataFlow::BarrierGuardNode, DataFlow::ValueNode {
|
||||
override InExpr astNode;
|
||||
|
||||
InExprGuard() {
|
||||
// Exclude tests of form `key in src` for the same reason as in HasOwnPropertyGuard
|
||||
not arePropertiesEnumerated(astNode.getRightOperand().flow().getALocalSource())
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e) {
|
||||
e = astNode.getLeftOperand() and outcome = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer guard for `instanceof` expressions.
|
||||
*
|
||||
* `Object.prototype instanceof X` is never true, so this blocks the `__proto__` label.
|
||||
*
|
||||
* It is still possible to get to `Function.prototype` through `constructor.constructor.prototype`
|
||||
* so we do not block the `constructor` label.
|
||||
*/
|
||||
class InstanceOfGuard extends DataFlow::LabeledBarrierGuardNode, DataFlow::ValueNode {
|
||||
override InstanceOfExpr astNode;
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
|
||||
e = astNode.getLeftOperand() and outcome = true and label = "__proto__"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer guard of form `typeof x === "object"` or `typeof x === "function"`.
|
||||
*
|
||||
* The former blocks the `constructor` label as that payload must pass through a function,
|
||||
* and the latter blocks the `__proto__` label as that only passes through objects.
|
||||
*/
|
||||
class TypeofGuard extends DataFlow::LabeledBarrierGuardNode, DataFlow::ValueNode {
|
||||
override EqualityTest astNode;
|
||||
TypeofExpr typeof;
|
||||
string typeofStr;
|
||||
|
||||
TypeofGuard() {
|
||||
typeof = astNode.getAnOperand() and
|
||||
typeofStr = astNode.getAnOperand().getStringValue()
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel label) {
|
||||
e = typeof.getOperand() and
|
||||
outcome = astNode.getPolarity() and
|
||||
(
|
||||
typeofStr = "object" and
|
||||
label = "constructor"
|
||||
or
|
||||
typeofStr = "function" and
|
||||
label = "__proto__"
|
||||
)
|
||||
or
|
||||
e = typeof.getOperand() and
|
||||
outcome = astNode.getPolarity().booleanNot() and
|
||||
(
|
||||
// If something is not an object, sanitize object, as both must end
|
||||
// in non-function prototype object.
|
||||
typeofStr = "object" and
|
||||
label instanceof UnsafePropLabel
|
||||
or
|
||||
typeofStr = "function" and
|
||||
label = "constructor"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A check of form `["__proto__"].includes(x)` or similar.
|
||||
*/
|
||||
class BlacklistInclusionGuard extends DataFlow::LabeledBarrierGuardNode, InclusionTest {
|
||||
UnsafePropLabel label;
|
||||
|
||||
BlacklistInclusionGuard() {
|
||||
exists(DataFlow::ArrayCreationNode array |
|
||||
array.getAnElement().getStringValue() = label and
|
||||
array.flowsTo(getContainerNode())
|
||||
)
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel lbl) {
|
||||
outcome = getPolarity().booleanNot() and
|
||||
e = getContainedNode().asExpr() and
|
||||
label = lbl
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A check of form `xs.includes(x)` or similar, which sanitizes `x` in the true case.
|
||||
*/
|
||||
class WhitelistInclusionGuard extends DataFlow::LabeledBarrierGuardNode {
|
||||
WhitelistInclusionGuard() {
|
||||
this instanceof TaintTracking::PositiveIndexOfSanitizer or
|
||||
this instanceof TaintTracking::InclusionSanitizer
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel lbl) {
|
||||
this.(TaintTracking::AdditionalSanitizerGuardNode).sanitizes(outcome, e) and
|
||||
lbl instanceof UnsafePropLabel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a meaningful name for `node` if possible.
|
||||
*/
|
||||
string getExprName(DataFlow::Node node) {
|
||||
result = node.asExpr().(Identifier).getName()
|
||||
or
|
||||
result = node.asExpr().(DotExpr).getPropertyName()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a name to display for `node`.
|
||||
*/
|
||||
string deriveExprName(DataFlow::Node node) {
|
||||
result = getExprName(node)
|
||||
or
|
||||
not exists(getExprName(node)) and
|
||||
result = "this object"
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the dynamic property write `base[prop] = rhs` can pollute the prototype
|
||||
* of `base` due to flow from `enum`.
|
||||
*
|
||||
* In most cases this will result in an alert, the exception being the case where
|
||||
* `base` does not have a prototype at all.
|
||||
*/
|
||||
predicate isPrototypePollutingAssignment(Node base, Node prop, Node rhs, EnumeratedPropName enum) {
|
||||
dynamicPropWrite(base, prop, rhs) and
|
||||
exists(PropNameTracking cfg |
|
||||
cfg.hasFlow(enum, base) and
|
||||
cfg.hasFlow(enum, prop) and
|
||||
cfg.hasFlow(enum.getASourceProp(), rhs)
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets a data flow node leading to the base of a prototype-polluting assignment. */
|
||||
private DataFlow::SourceNode getANodeLeadingToBase(DataFlow::TypeBackTracker t, Node base) {
|
||||
t.start() and
|
||||
isPrototypePollutingAssignment(base, _, _, _) and
|
||||
result = base.getALocalSource()
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 |
|
||||
result = getANodeLeadingToBase(t2, base).backtrack(t2, t)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a data flow node leading to the base of dynamic property read leading to a
|
||||
* prototype-polluting assignment.
|
||||
*
|
||||
* For example, this is the `dst` in `dst[key1][key2] = ...`.
|
||||
* This dynamic read is where the reference to a built-in prototype object is obtained,
|
||||
* and we need this to ensure that this object actually has a prototype.
|
||||
*/
|
||||
private DataFlow::SourceNode getANodeLeadingToBaseBase(DataFlow::TypeBackTracker t, Node base) {
|
||||
exists(DynamicPropRead read |
|
||||
read = getANodeLeadingToBase(t, base) and
|
||||
result = read.getBase().getALocalSource()
|
||||
)
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 |
|
||||
result = getANodeLeadingToBaseBase(t2, base).backtrack(t2, t)
|
||||
)
|
||||
}
|
||||
|
||||
DataFlow::SourceNode getANodeLeadingToBaseBase(Node base) {
|
||||
result = getANodeLeadingToBaseBase(DataFlow::TypeBackTracker::end(), base)
|
||||
}
|
||||
|
||||
/** A call to `Object.create(null)`. */
|
||||
class ObjectCreateNullCall extends CallNode {
|
||||
ObjectCreateNullCall() {
|
||||
this = globalVarRef("Object").getAMemberCall("create") and
|
||||
getArgument(0).asExpr() instanceof NullLiteral
|
||||
}
|
||||
}
|
||||
|
||||
from
|
||||
PropNameTracking cfg, DataFlow::PathNode source, DataFlow::PathNode sink, EnumeratedPropName enum,
|
||||
Node base
|
||||
where
|
||||
cfg.hasFlowPath(source, sink) and
|
||||
isPrototypePollutingAssignment(base, _, _, enum) and
|
||||
sink.getNode() = base and
|
||||
source.getNode() = enum and
|
||||
(
|
||||
getANodeLeadingToBaseBase(base) instanceof ObjectLiteralNode
|
||||
or
|
||||
not getANodeLeadingToBaseBase(base) instanceof ObjectCreateNullCall
|
||||
)
|
||||
select base, source, sink,
|
||||
"Properties are copied from $@ to $@ without guarding against prototype pollution.",
|
||||
enum.getSourceObject(), deriveExprName(enum.getSourceObject()), base, deriveExprName(base)
|
||||
@@ -0,0 +1,10 @@
|
||||
function merge(dst, src) {
|
||||
for (let key in src) {
|
||||
if (!src.hasOwnProperty(key)) continue;
|
||||
if (isObject(dst[key])) {
|
||||
merge(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function merge(dst, src) {
|
||||
for (let key in src) {
|
||||
if (!src.hasOwnProperty(key)) continue;
|
||||
if (dst.hasOwnProperty(key) && isObject(dst[key])) {
|
||||
merge(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
function merge(dst, src) {
|
||||
for (let key in src) {
|
||||
if (!src.hasOwnProperty(key)) continue;
|
||||
if (key === "__proto__" || key === "constructor") continue;
|
||||
if (isObject(dst[key])) {
|
||||
merge(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ import semmle.javascript.frameworks.Credentials
|
||||
import semmle.javascript.frameworks.CryptoLibraries
|
||||
import semmle.javascript.frameworks.DigitalOcean
|
||||
import semmle.javascript.frameworks.Electron
|
||||
import semmle.javascript.frameworks.EventEmitter
|
||||
import semmle.javascript.frameworks.Files
|
||||
import semmle.javascript.frameworks.Firebase
|
||||
import semmle.javascript.frameworks.jQuery
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import javascript
|
||||
private import semmle.javascript.dataflow.InferredTypes
|
||||
|
||||
deprecated
|
||||
module GlobalAccessPath {
|
||||
@@ -34,6 +35,24 @@ module GlobalAccessPath {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides predicates for associating access paths with data flow nodes.
|
||||
*
|
||||
* For example, `AccessPath.getAReferenceTo(x)` can be used to obtain the global access path
|
||||
* that `x` refers to, as in the following sample:
|
||||
* ```
|
||||
* function f() {
|
||||
* let v = foo.bar; // reference to 'foo.bar'
|
||||
* v.baz; // reference to 'foo.bar.baz'
|
||||
* }
|
||||
*
|
||||
* (function(ns) {
|
||||
* ns.x; // reference to 'NS.x'
|
||||
* })(NS = NS || {});
|
||||
* ```
|
||||
*
|
||||
* A pseudo-property named `[number]` is sometimes used to represent array indices within an access path.
|
||||
*/
|
||||
module AccessPath {
|
||||
/**
|
||||
* A source node that can be the root of an access path.
|
||||
@@ -80,6 +99,35 @@ module AccessPath {
|
||||
result = base + "." + prop
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `variable` is compared to the `length` property of something, indicating
|
||||
* that, if used as a dynamic property name, it represents an array index.
|
||||
*/
|
||||
private predicate isLikelyArrayIndex(SsaVariable variable) {
|
||||
exists(RelationalComparison cmp, DataFlow::PropRead length, Expr lengthUse |
|
||||
length.getPropertyName() = "length" and
|
||||
length.flowsToExpr(lengthUse) and
|
||||
cmp.hasOperands(variable.getAUse(), lengthUse)
|
||||
)
|
||||
or
|
||||
isLikelyArrayIndex(variable.getDefinition().(SsaRefinementNode).getAnInput())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `prop` likely accesses a non-constant array element.
|
||||
*/
|
||||
private predicate isLikelyDynamicArrayAccess(DataFlow::PropRead prop) {
|
||||
// The implicit PropRead in a for-of loop is represented by its lvalue node
|
||||
prop = DataFlow::lvalueNode(any(ForOfStmt stmt).getLValue())
|
||||
or
|
||||
// Match an index access x[i] where `i` is likely an array index variable.
|
||||
not exists(prop.getPropertyName()) and
|
||||
exists(SsaVariable indexVar |
|
||||
isLikelyArrayIndex(indexVar) and
|
||||
prop.getPropertyNameExpr() = indexVar.getAUse()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the access path relative to `root` referred to by `node`.
|
||||
*
|
||||
@@ -115,6 +163,9 @@ module AccessPath {
|
||||
not node.accessesGlobal(_) and
|
||||
exists(DataFlow::PropRead prop | node = prop |
|
||||
result = join(fromReference(prop.getBase(), root), prop.getPropertyName())
|
||||
or
|
||||
isLikelyDynamicArrayAccess(prop) and
|
||||
result = join(fromReference(prop.getBase(), root), "[number]")
|
||||
)
|
||||
or
|
||||
exists(Closure::ClosureNamespaceAccess acc | node = acc |
|
||||
|
||||
@@ -73,187 +73,109 @@ module Electron {
|
||||
/**
|
||||
* A reference to the `webContents` property of a browser object.
|
||||
*/
|
||||
class WebContents extends DataFlow::SourceNode {
|
||||
class WebContents extends DataFlow::SourceNode, NodeJSLib::NodeJSEventEmitter {
|
||||
WebContents() { this.(DataFlow::PropRead).accesses(any(BrowserObject bo), "webContents") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides classes and predicates for modelling Electron inter-process communication (IPC).
|
||||
* The Electron IPC are EventEmitters, but they also expose a number of methods on top of the standard EventEmitter.
|
||||
*/
|
||||
private module IPC {
|
||||
class Process extends string {
|
||||
Process() { this = "main" or this = "renderer" }
|
||||
DataFlow::SourceNode main() { result = DataFlow::moduleMember("electron", "ipcMain") }
|
||||
|
||||
DataFlow::SourceNode getAnImport() {
|
||||
this = Process::main() and result = DataFlow::moduleMember("electron", "ipcMain")
|
||||
DataFlow::SourceNode renderer() { result = DataFlow::moduleMember("electron", "ipcRenderer") }
|
||||
|
||||
/**
|
||||
* A model for the Main and Renderer process in an Electron app.
|
||||
*/
|
||||
abstract class Process extends EventEmitter::Range {
|
||||
/**
|
||||
* Gets a node that refers to a Process object.
|
||||
*/
|
||||
DataFlow::SourceNode ref() { result = EventEmitter::trackEventEmitter(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of the Main process of an Electron app.
|
||||
* Communication in an Electron app generally happens from the renderer process to the main process.
|
||||
*/
|
||||
class MainProcess extends Process {
|
||||
MainProcess() { this = main() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of the renderer process of an Electron app.
|
||||
*/
|
||||
class RendererProcess extends Process {
|
||||
RendererProcess() { this = renderer() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The `sender` property of the event in an IPC event handler.
|
||||
* This sender is used to send a response back from the main process to the renderer.
|
||||
*/
|
||||
class ProcessSender extends Process {
|
||||
ProcessSender() {
|
||||
exists(IPCSendRegistration reg | reg.getEmitter() instanceof MainProcess |
|
||||
this = reg.getABoundCallbackParameter(1, 0).getAPropertyRead("sender")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A registration of an Electron IPC event handler.
|
||||
* Does mostly the same as an EventEmitter event handler,
|
||||
* except that values can be returned through the `event.returnValue` property.
|
||||
*/
|
||||
class IPCSendRegistration extends EventRegistration::DefaultEventRegistration,
|
||||
DataFlow::MethodCallNode {
|
||||
override Process emitter;
|
||||
|
||||
IPCSendRegistration() { this = emitter.ref().getAMethodCall(EventEmitter::on()) }
|
||||
|
||||
override DataFlow::Node getAReturnedValue() {
|
||||
result = this.getABoundCallbackParameter(1, 0).getAPropertyWrite("returnValue").getRhs()
|
||||
}
|
||||
|
||||
override IPCDispatch getAReturnDispatch() {
|
||||
result.getCalleeName() = "sendSync"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dispatch of an IPC event.
|
||||
* An IPC event is sent from the renderer to the main process.
|
||||
* And a value can be returned through the `returnValue` property of the event (first parameter in the callback).
|
||||
*/
|
||||
class IPCDispatch extends EventDispatch::DefaultEventDispatch, DataFlow::InvokeNode {
|
||||
override Process emitter;
|
||||
|
||||
IPCDispatch() {
|
||||
exists(string methodName | methodName = "sendSync" or methodName = "send" |
|
||||
this = emitter.ref().getAMemberCall(methodName)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `i`th dispatched argument to the event handler.
|
||||
* The 0th parameter in the callback is a event generated by the IPC system,
|
||||
* therefore these arguments start at 1.
|
||||
*/
|
||||
override DataFlow::Node getSentItem(int i) {
|
||||
i >= 1 and
|
||||
result = getArgument(i)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a registration that this dispatch can send an event to.
|
||||
*/
|
||||
override IPCSendRegistration getAReceiver() {
|
||||
this.getEmitter() instanceof RendererProcess and
|
||||
result.getEmitter() instanceof MainProcess
|
||||
or
|
||||
this = Process::renderer() and result = DataFlow::moduleMember("electron", "ipcRenderer")
|
||||
}
|
||||
}
|
||||
|
||||
module Process {
|
||||
Process main() { result = "main" }
|
||||
|
||||
Process renderer() { result = "renderer" }
|
||||
}
|
||||
|
||||
/**
|
||||
* An IPC callback.
|
||||
*/
|
||||
class Callback extends DataFlow::FunctionNode {
|
||||
DataFlow::Node channel;
|
||||
Process process;
|
||||
|
||||
Callback() {
|
||||
exists(DataFlow::MethodCallNode mc |
|
||||
mc = process.getAnImport().getAMemberCall("on") and
|
||||
this = mc.getCallback(1) and
|
||||
channel = mc.getArgument(0)
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the process on which this callback is executed. */
|
||||
Process getProcess() { result = process }
|
||||
|
||||
/** Gets the name of the channel the callback is listening on. */
|
||||
string getChannelName() { result = channel.getStringValue() }
|
||||
|
||||
/** Gets the data flow node containing the message received by the callback. */
|
||||
DataFlow::Node getMessage() { result = getParameter(1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An IPC message.
|
||||
*/
|
||||
abstract class Message extends DataFlow::Node {
|
||||
/** Gets the process that sends this message. */
|
||||
abstract Process getProcess();
|
||||
|
||||
/** Gets the name of the channel this message is sent on. */
|
||||
abstract string getChannelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* An IPC message sent directly from a process.
|
||||
*/
|
||||
class DirectMessage extends Message {
|
||||
DataFlow::MethodCallNode mc;
|
||||
Process process;
|
||||
DataFlow::Node channel;
|
||||
boolean isSync;
|
||||
|
||||
DirectMessage() {
|
||||
exists(string send |
|
||||
send = "send" and isSync = false
|
||||
or
|
||||
send = "sendSync" and isSync = true
|
||||
|
|
||||
mc = process.getAnImport().getAMemberCall(send) and
|
||||
this = mc.getArgument(1) and
|
||||
channel = mc.getArgument(0)
|
||||
)
|
||||
}
|
||||
|
||||
override Process getProcess() { result = process }
|
||||
|
||||
override string getChannelName() { result = channel.getStringValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A synchronous IPC message sent directly from a process.
|
||||
*/
|
||||
class SyncDirectMessage extends DirectMessage {
|
||||
SyncDirectMessage() { isSync = true }
|
||||
|
||||
/** Gets the data flow node holding the reply to the message. */
|
||||
DataFlow::Node getReply() { result = mc }
|
||||
}
|
||||
|
||||
/**
|
||||
* An asynchronous IPC reply sent from within an IPC callback.
|
||||
*/
|
||||
class AsyncReplyMessage extends Message {
|
||||
Callback callback;
|
||||
DataFlow::Node channel;
|
||||
|
||||
AsyncReplyMessage() {
|
||||
exists(DataFlow::MethodCallNode mc |
|
||||
mc = callback.getParameter(0).getAPropertyRead("sender").getAMemberCall("send") and
|
||||
this = mc.getArgument(1) and
|
||||
channel = mc.getArgument(0)
|
||||
)
|
||||
}
|
||||
|
||||
override Process getProcess() { result = callback.getProcess() }
|
||||
|
||||
override string getChannelName() { result = channel.getStringValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A synchronous IPC reply sent from within an IPC callback.
|
||||
*/
|
||||
class SyncReplyMessage extends Message {
|
||||
Callback callback;
|
||||
|
||||
SyncReplyMessage() {
|
||||
this = callback.getParameter(0).getAPropertyWrite("returnValue").getRhs()
|
||||
}
|
||||
|
||||
override Process getProcess() { result = callback.getProcess() }
|
||||
|
||||
override string getChannelName() { result = callback.getChannelName() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An asynchronous Electron IPC message sent from the main process via a `webContents` object.
|
||||
*/
|
||||
class WebContentsMessage extends Message {
|
||||
DataFlow::Node channel;
|
||||
|
||||
WebContentsMessage() {
|
||||
exists(WebContents wc, DataFlow::MethodCallNode mc |
|
||||
wc.flowsTo(mc.getReceiver()) and
|
||||
this = mc.getArgument(1) and
|
||||
channel = mc.getArgument(0) and
|
||||
mc.getCalleeName() = "send"
|
||||
)
|
||||
}
|
||||
|
||||
override Process getProcess() { result = Process::main() }
|
||||
|
||||
override string getChannelName() { result = channel.getStringValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `pred` flows to `succ` via Electron IPC.
|
||||
*/
|
||||
private predicate ipcFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
// match a message sent from one process with a callback parameter in the other process
|
||||
exists(Callback callback, Message msg |
|
||||
callback.getChannelName() = msg.getChannelName() and
|
||||
callback.getProcess() != msg.getProcess() and
|
||||
pred = msg and
|
||||
succ = callback.getMessage()
|
||||
)
|
||||
or
|
||||
// match a synchronous reply sent from one process with a `sendSync` call in the other process
|
||||
exists(SyncDirectMessage sendSync, SyncReplyMessage msg |
|
||||
sendSync.getChannelName() = msg.getChannelName() and
|
||||
sendSync.getProcess() != msg.getProcess() and
|
||||
pred = msg and
|
||||
succ = sendSync.getReply()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An additional flow step via an Electron IPC message.
|
||||
*/
|
||||
private class IPCAdditionalFlowStep extends DataFlow::AdditionalFlowStep {
|
||||
IPCAdditionalFlowStep() { ipcFlowStep(this, _) }
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
pred = this and
|
||||
ipcFlowStep(pred, succ)
|
||||
this.getEmitter() instanceof ProcessSender and
|
||||
result.getEmitter() instanceof RendererProcess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
229
javascript/ql/src/semmle/javascript/frameworks/EventEmitter.qll
Normal file
229
javascript/ql/src/semmle/javascript/frameworks/EventEmitter.qll
Normal file
@@ -0,0 +1,229 @@
|
||||
import javascript
|
||||
|
||||
module EventEmitter {
|
||||
/** Gets the name of a method on `EventEmitter` that returns `this`. */
|
||||
string chainableMethod() {
|
||||
result = "off" or
|
||||
result = "removeAllListeners" or
|
||||
result = "removeListener" or
|
||||
result = "setMaxListeners" or
|
||||
result = on()
|
||||
}
|
||||
|
||||
/** Gets the name of a method on `EventEmitter` that registers an event handler. */
|
||||
string on() {
|
||||
result = "addListener" or
|
||||
result = "on" or
|
||||
result = "once" or
|
||||
result = "prependListener" or
|
||||
result = "prependOnceListener"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node that refers to an EventEmitter object.
|
||||
*/
|
||||
DataFlow::SourceNode trackEventEmitter(EventEmitter emitter) {
|
||||
result = trackEventEmitter(DataFlow::TypeTracker::end(), emitter)
|
||||
}
|
||||
|
||||
private DataFlow::SourceNode trackEventEmitter(DataFlow::TypeTracker t, EventEmitter emitter) {
|
||||
t.start() and result = emitter
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2, DataFlow::SourceNode pred |
|
||||
pred = trackEventEmitter(t2, emitter)
|
||||
|
|
||||
result = pred.track(t2, t)
|
||||
or
|
||||
// invocation of a chainable method
|
||||
exists(DataFlow::MethodCallNode mcn |
|
||||
mcn = pred.getAMethodCall(chainableMethod()) and
|
||||
// exclude getter versions
|
||||
exists(mcn.getAnArgument()) and
|
||||
result = mcn and
|
||||
t = t2.continue()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that implements the EventEmitter API.
|
||||
* Extending this class does nothing, its mostly to indicate intent.
|
||||
*
|
||||
* The classes EventRegistration::Range and EventDispatch::Range must be extended to get a working EventEmitter model.
|
||||
* An EventRegistration models a method call that registers some event handler on an EventEmitter.
|
||||
* And EventDispatch models that some event is dispatched on an EventEmitter.
|
||||
*
|
||||
* Both the EventRegistration and EventDispatch have a field `emitter`,
|
||||
* which is the EventEmitter that events are registered on / dispatched to respectively.
|
||||
*
|
||||
* Here is a simple JavaScript example with the NodeJS EventEmitter:
|
||||
* var e = new EventEmitter(); // <- EventEmitter
|
||||
* e.on("name", (data) => {...}); // <- EventRegistration
|
||||
* e.emit("name", "foo"); // <- EventDispatch
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node { }
|
||||
}
|
||||
|
||||
/**
|
||||
* An EventEmitter instance that implements the EventEmitter API.
|
||||
* Extend EventEmitter::Range to mark something as being an EventEmitter.
|
||||
*/
|
||||
class EventEmitter extends DataFlow::Node {
|
||||
EventEmitter::Range range;
|
||||
|
||||
EventEmitter() { this = range }
|
||||
}
|
||||
|
||||
/**
|
||||
* A registration of an event handler on an EventEmitter.
|
||||
*/
|
||||
class EventRegistration extends DataFlow::Node {
|
||||
EventRegistration::Range range;
|
||||
|
||||
EventRegistration() { this = range }
|
||||
|
||||
/** Gets the EventEmitter that the event handler is registered on. */
|
||||
final EventEmitter getEmitter() { result = range.getEmitter() }
|
||||
|
||||
/** Gets the name of the channel if possible. */
|
||||
string getChannel() { result = range.getChannel() }
|
||||
|
||||
/** Gets the `i`th parameter in the event handler. */
|
||||
DataFlow::Node getReceivedItem(int i) { result = range.getReceivedItem(i) }
|
||||
|
||||
/**
|
||||
* Gets a value that is returned by the event handler.
|
||||
* The default implementation is that no value can be returned.
|
||||
*/
|
||||
DataFlow::Node getAReturnedValue() { result = range.getAReturnedValue() }
|
||||
|
||||
/**
|
||||
* Get a dispatch that this event handler can return a value to.
|
||||
* The default implementation is that there exists no such dispatch.
|
||||
*/
|
||||
EventDispatch getAReturnDispatch() { result = range.getAReturnDispatch() }
|
||||
}
|
||||
|
||||
module EventRegistration {
|
||||
/**
|
||||
* A registration of an event handler on an EventEmitter.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
EventEmitter::Range emitter;
|
||||
|
||||
final EventEmitter getEmitter() { result = emitter }
|
||||
|
||||
abstract string getChannel();
|
||||
|
||||
abstract DataFlow::Node getReceivedItem(int i);
|
||||
|
||||
abstract DataFlow::Node getAReturnedValue();
|
||||
|
||||
abstract EventDispatch::Range getAReturnDispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* A default implementation of an EventRegistration.
|
||||
* The default implementation assumes that `this` is a DataFlow::InvokeNode where the
|
||||
* first argument is a string describing which channel is registered, and the second
|
||||
* argument is the event handler callback.
|
||||
*/
|
||||
abstract class DefaultEventRegistration extends Range, DataFlow::InvokeNode {
|
||||
override string getChannel() {
|
||||
this.getArgument(0).mayHaveStringValue(result)
|
||||
}
|
||||
|
||||
override DataFlow::Node getReceivedItem(int i) {
|
||||
result = this.getABoundCallbackParameter(1, i)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAReturnedValue() { none() }
|
||||
|
||||
override EventDispatch::Range getAReturnDispatch() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dispatch of an event on an EventEmitter.
|
||||
*/
|
||||
class EventDispatch extends DataFlow::Node {
|
||||
EventDispatch::Range range;
|
||||
|
||||
EventDispatch() { this = range }
|
||||
|
||||
/** Gets the emitter that the event dispatch happens on. */
|
||||
EventEmitter getEmitter() { result = range.getEmitter() }
|
||||
|
||||
/** Gets the name of the channel if possible. */
|
||||
string getChannel() { result = range.getChannel() }
|
||||
|
||||
/** Gets the `i`th argument that is send to the event handler. */
|
||||
DataFlow::Node getSentItem(int i) { result = range.getSentItem(i) }
|
||||
|
||||
/**
|
||||
* Get an EventRegistration that this event dispatch can send an event to.
|
||||
* The default implementation is that the emitters of the dispatch and registration have to be equal.
|
||||
* Channels are by default ignored.
|
||||
*/
|
||||
EventRegistration getAReceiver() { result = range.getAReceiver() }
|
||||
}
|
||||
|
||||
module EventDispatch {
|
||||
/**
|
||||
* A dispatch of an event on an EventEmitter.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
EventEmitter::Range emitter;
|
||||
|
||||
final EventEmitter getEmitter() { result = emitter }
|
||||
|
||||
abstract string getChannel();
|
||||
|
||||
abstract DataFlow::Node getSentItem(int i);
|
||||
|
||||
abstract EventRegistration::Range getAReceiver();
|
||||
}
|
||||
|
||||
/**
|
||||
* A default implementation of an EventDispatch.
|
||||
* The default implementation assumes that the dispatch is a DataFlow::InvokeNode,
|
||||
* where the first argument is a string describing the channel, and the `i`+1 argument
|
||||
* is the `i`th item sent to the event handler.
|
||||
*/
|
||||
abstract class DefaultEventDispatch extends Range, DataFlow::InvokeNode {
|
||||
override string getChannel() {
|
||||
this.getArgument(0).mayHaveStringValue(result)
|
||||
}
|
||||
|
||||
override DataFlow::Node getSentItem(int i) {
|
||||
result = this.getArgument(i + 1)
|
||||
}
|
||||
|
||||
override EventRegistration::Range getAReceiver() { this.getEmitter() = result.getEmitter() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-step that models data-flow between event handlers and event dispatchers.
|
||||
*/
|
||||
private class EventEmitterTaintStep extends DataFlow::AdditionalFlowStep {
|
||||
EventRegistration reg;
|
||||
EventDispatch dispatch;
|
||||
|
||||
EventEmitterTaintStep() {
|
||||
this = dispatch and
|
||||
reg = dispatch.getAReceiver() and
|
||||
not dispatch.getChannel() != reg.getChannel()
|
||||
}
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(int i | i >= 0 |
|
||||
pred = dispatch.getSentItem(i) and
|
||||
succ = reg.getReceivedItem(i)
|
||||
)
|
||||
or
|
||||
dispatch = reg.getAReturnDispatch() and
|
||||
pred = reg.getAReturnedValue() and
|
||||
succ = dispatch
|
||||
}
|
||||
}
|
||||
@@ -880,4 +880,49 @@ module NodeJSLib {
|
||||
|
||||
override string getSourceType() { result = "NodeJSClientRequest error event" }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An NodeJS EventEmitter instance.
|
||||
* Events dispatched on this EventEmitter will be handled by event handlers registered on this EventEmitter.
|
||||
* (That is opposed to e.g. SocketIO, which implements the same interface, but where events cross object boundaries).
|
||||
*/
|
||||
abstract class NodeJSEventEmitter extends EventEmitter::Range {
|
||||
/**
|
||||
* Get a Node that refers to a NodeJS EventEmitter instance.
|
||||
*/
|
||||
DataFlow::SourceNode ref() { result = EventEmitter::trackEventEmitter(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of an EventEmitter that is imported through the 'events' module.
|
||||
*/
|
||||
private class ImportedNodeJSEventEmitter extends NodeJSEventEmitter {
|
||||
ImportedNodeJSEventEmitter() {
|
||||
exists(DataFlow::SourceNode clazz |
|
||||
clazz = DataFlow::moduleImport("events") or
|
||||
clazz = DataFlow::moduleMember("events", "EventEmitter")
|
||||
|
|
||||
this = clazz.getAnInstantiation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A registration of an event handler on a NodeJS EventEmitter instance.
|
||||
*/
|
||||
private class NodeJSEventRegistration extends EventRegistration::DefaultEventRegistration, DataFlow::MethodCallNode {
|
||||
override NodeJSEventEmitter emitter;
|
||||
|
||||
NodeJSEventRegistration() { this = emitter.ref().getAMethodCall(EventEmitter::on()) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dispatch of an event on a NodeJS EventEmitter instance.
|
||||
*/
|
||||
private class NodeJSEventDispatch extends EventDispatch::DefaultEventDispatch, DataFlow::MethodCallNode {
|
||||
override NodeJSEventEmitter emitter;
|
||||
|
||||
NodeJSEventDispatch() { this = emitter.ref().getAMethodCall("emit") }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,27 +574,6 @@ module SocketIOClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides predicates for working with Node.js `EventEmitter`s. */
|
||||
private module EventEmitter {
|
||||
/** Gets the name of a method on `EventEmitter` that returns `this`. */
|
||||
string chainableMethod() {
|
||||
result = "off" or
|
||||
result = "removeAllListeners" or
|
||||
result = "removeListener" or
|
||||
result = "setMaxListeners" or
|
||||
result = on()
|
||||
}
|
||||
|
||||
/** Gets the name of a method on `EventEmitter` that registers an event handler. */
|
||||
string on() {
|
||||
result = "addListener" or
|
||||
result = "on" or
|
||||
result = "once" or
|
||||
result = "prependListener" or
|
||||
result = "prependOnceListener"
|
||||
}
|
||||
}
|
||||
|
||||
/** A data flow step through socket.io sockets. */
|
||||
private class SocketIoStep extends DataFlow::AdditionalFlowStep {
|
||||
DataFlow::Node pred;
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
| electron.js:39:5:39:6 | bw |
|
||||
| electron.js:40:1:40:7 | foo(bv) |
|
||||
| electron.js:40:5:40:6 | bv |
|
||||
| electron.js:62:7:62:59 | win |
|
||||
| electron.js:62:13:62:59 | new Bro ... 1500 }) |
|
||||
| electron.js:63:3:63:5 | win |
|
||||
| electron.js:65:18:65:20 | win |
|
||||
| electron.ts:3:12:3:13 | bw |
|
||||
| electron.ts:3:40:3:41 | bv |
|
||||
| electron.ts:4:3:4:4 | bw |
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
| electron.js:48:23:48:28 | 'pong' | electron.js:58:1:58:36 | ipcRend ... 'ping') |
|
||||
| electron.js:56:27:56:32 | 'ping' | electron.js:42:29:42:31 | arg |
|
||||
| electron.js:58:30:58:35 | 'ping' | electron.js:47:28:47:30 | arg |
|
||||
| electron.js:68:24:68:28 | "foo" | electron.js:67:23:67:25 | foo |
|
||||
| electron.js:69:24:69:28 | "bar" | electron.js:67:46:67:48 | bar |
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
| electron.js:39:1:39:19 | foo(bw).webContents |
|
||||
| electron.js:40:1:40:19 | foo(bv).webContents |
|
||||
| electron.js:65:18:65:32 | win.webContents |
|
||||
| electron.ts:4:3:4:16 | bw.webContents |
|
||||
| electron.ts:5:3:5:16 | bv.webContents |
|
||||
|
||||
@@ -56,3 +56,15 @@ ipcRenderer.on('reply', (event, arg) => {
|
||||
ipcRenderer.send('async', 'ping');
|
||||
|
||||
ipcRenderer.sendSync('sync', 'ping');
|
||||
|
||||
|
||||
(function () {
|
||||
let win = new BrowserWindow({ width: 800, height: 1500 })
|
||||
win.loadURL('http://github.com');
|
||||
|
||||
let contents = win.webContents;
|
||||
|
||||
contents.on("foo", (foo) => {}).on("bar", (bar) => {});
|
||||
contents.emit("foo", "foo");
|
||||
contents.emit("bar", "bar");
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
| tst.js:9:23:9:33 | 'FirstData' | tst.js:6:40:6:44 | first |
|
||||
| tst.js:10:24:10:35 | 'SecondData' | tst.js:7:32:7:37 | second |
|
||||
| tst.js:15:24:15:39 | 'OtherFirstData' | tst.js:14:41:14:50 | otherFirst |
|
||||
| tst.js:20:17:20:21 | "foo" | tst.js:19:16:19:18 | foo |
|
||||
| tst.js:21:17:21:21 | "bar" | tst.js:19:39:19:41 | bar |
|
||||
| tst.js:28:17:28:22 | "blab" | tst.js:25:16:25:20 | event |
|
||||
@@ -0,0 +1,8 @@
|
||||
import javascript
|
||||
|
||||
query predicate taintSteps(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(DataFlow::AdditionalFlowStep step |
|
||||
step.step(pred, succ)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
var emitter = require('events').EventEmitter;
|
||||
|
||||
var em = new emitter();
|
||||
|
||||
// Splitting different channels
|
||||
em.addListener('FirstEvent', function (first) {});
|
||||
em.on('SecondEvent', function (second) {});
|
||||
|
||||
em.emit('FirstEvent', 'FirstData');
|
||||
em.emit('SecondEvent', 'SecondData');
|
||||
|
||||
// Splitting different emitters.
|
||||
var em2 = new emitter();
|
||||
em2.addListener('FirstEvent', function (otherFirst) {});
|
||||
em2.emit('FirstEvent', 'OtherFirstData');
|
||||
|
||||
// Chaining.
|
||||
var em3 = new emitter();
|
||||
em3.on("foo", (foo) => {}).on("bar", (bar) => {});
|
||||
em3.emit("foo", "foo");
|
||||
em3.emit("bar", "bar");
|
||||
|
||||
// Returning a value does not work here.
|
||||
var em4 = new emitter();
|
||||
em3.on("bla", (event) => {
|
||||
event.returnValue = "foo"
|
||||
});
|
||||
em3.emit("bla", "blab");
|
||||
@@ -1,3 +1,4 @@
|
||||
| try.js:22:9:22:26 | x.ordinaryProperty | This expression has no effect. |
|
||||
| tst2.js:3:4:3:4 | 0 | This expression has no effect. |
|
||||
| tst.js:3:1:3:2 | 23 | This expression has no effect. |
|
||||
| tst.js:5:1:5:2 | 23 | This expression has no effect. |
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
function try1(x) {
|
||||
try {
|
||||
x.ordinaryProperty; // OK - try/catch indicates intent to throw exception
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function try2(x) {
|
||||
try {
|
||||
x.ordinaryProperty; // OK - try/catch indicates intent to throw exception
|
||||
return x;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function try3(x) {
|
||||
try {
|
||||
x.ordinaryProperty()
|
||||
x.ordinaryProperty // NOT OK
|
||||
return x;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
| tst.js:5:30:5:45 | arguments.callee | Strict mode code cannot use arguments.callee. |
|
||||
| tst.js:7:21:7:36 | arguments.callee | Strict mode code cannot use arguments.callee. |
|
||||
| tst.js:9:20:9:27 | f.caller | Strict mode code cannot use Function.prototype.caller. |
|
||||
| tst.js:11:8:11:18 | f.arguments | Strict mode code cannot use Function.prototype.arguments. |
|
||||
| tst.js:18:3:18:18 | arguments.callee | Strict mode code cannot use arguments.callee. |
|
||||
| tst.js:31:5:31:14 | foo.caller | Strict mode code cannot use Function.prototype.caller. |
|
||||
| tst.js:31:5:31:14 | foo.caller | Strict mode code cannot use arguments.caller. |
|
||||
| tst.js:11:17:11:27 | f.arguments | Strict mode code cannot use Function.prototype.arguments. |
|
||||
| tst.js:18:10:18:25 | arguments.callee | Strict mode code cannot use arguments.callee. |
|
||||
| tst.js:31:12:31:21 | foo.caller | Strict mode code cannot use Function.prototype.caller. |
|
||||
| tst.js:31:12:31:21 | foo.caller | Strict mode code cannot use arguments.caller. |
|
||||
|
||||
@@ -8,14 +8,14 @@ var o = {
|
||||
// BAD
|
||||
console.log(f.caller);
|
||||
// BAD
|
||||
f.arguments;
|
||||
this.y = f.arguments;
|
||||
this.x = x;
|
||||
}
|
||||
};
|
||||
|
||||
var D = class extends function() {
|
||||
// BAD
|
||||
arguments.callee;
|
||||
return arguments.callee;
|
||||
} {};
|
||||
|
||||
function g() {
|
||||
@@ -28,6 +28,11 @@ function g() {
|
||||
function h() {
|
||||
var foo = Math.random() > 0.5 ? h : arguments;
|
||||
// BAD
|
||||
foo.caller;
|
||||
return foo.caller;
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
arguments.caller; // OK - avoid duplicate alert from useless-expression
|
||||
})();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
Security/CWE-400/PrototypePollutionUtility.ql
|
||||
@@ -0,0 +1,362 @@
|
||||
import dummy from 'somewhere';
|
||||
|
||||
function copyUsingForIn(dst, src) {
|
||||
for (let key in src) {
|
||||
if (dst[key]) {
|
||||
copyUsingForIn(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyUsingKeys(dst, src) {
|
||||
Object.keys(src).forEach(key => {
|
||||
if (dst[key]) {
|
||||
copyUsingKeys(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyRest(dst, ...sources) {
|
||||
for (let source of sources) {
|
||||
for (let key in source) {
|
||||
copyRestAux(dst, source[key], key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyRestAux(dst, value, key) {
|
||||
let dstValue = dst[key];
|
||||
if (dstValue) {
|
||||
copyRest(dstValue, value);
|
||||
} else {
|
||||
dst[key] = value; // NOT OK
|
||||
}
|
||||
}
|
||||
|
||||
function copyProtoGuarded(dst, src) {
|
||||
for (let key in src) {
|
||||
if (key === "__proto__") continue;
|
||||
if (dst[key]) {
|
||||
copyProtoGuarded(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyCtorGuarded(dst, src) {
|
||||
for (let key in src) {
|
||||
if (key === "constructor") continue;
|
||||
if (dst[key]) {
|
||||
copyCtorGuarded(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyDoubleGuarded(dst, src) {
|
||||
for (let key in src) {
|
||||
if (key === "constructor" || key === "__proto__") continue;
|
||||
if (dst[key]) {
|
||||
copyDoubleGuarded(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSafe(key) {
|
||||
return key !== "__proto__" && key !== "constructor" && key !== "prototype";
|
||||
}
|
||||
|
||||
function copyComplex(dst, src) {
|
||||
for (let key in src) {
|
||||
if (isSafe(key)) {
|
||||
if (dst[key]) {
|
||||
copyComplex(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyHasOwnProperty(dst, src) {
|
||||
for (let key in src) {
|
||||
// Guarding the recursive case by dst.hasOwnProperty is safe,
|
||||
// since '__proto__' and 'constructor' are not own properties of the destination object.
|
||||
if (dst.hasOwnProperty(key)) {
|
||||
copyHasOwnProperty(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyHasOwnPropertyBad(dst, src) {
|
||||
for (let key in src) {
|
||||
// Guarding using src.hasOwnProperty is *not* effective,
|
||||
// since '__proto__' and 'constructor' are own properties in the payload.
|
||||
if (!src.hasOwnProperty(key)) continue; // Not safe
|
||||
if (dst[key]) {
|
||||
copyHasOwnPropertyBad(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
|
||||
function copyHasOwnPropertyTearOff(dst, src) {
|
||||
for (let key in src) {
|
||||
if (_hasOwnProp.call(dst, key)) {
|
||||
copyHasOwnPropertyTearOff(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shallowExtend(dst, src) {
|
||||
for (let key in src) {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
|
||||
function transform(src, fn) {
|
||||
if (typeof src !== 'object') return fn(src);
|
||||
for (let key in src) {
|
||||
src[key] = transform(src[key], fn); // OK
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
function clone(src) {
|
||||
if (typeof src !== 'object') return src;
|
||||
let result = {};
|
||||
for (let key in src) {
|
||||
result[key] = clone(src[key]); // OK
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function higherOrderRecursion(dst, src, callback) {
|
||||
for (let key in src) {
|
||||
if (dst[key]) {
|
||||
callback(dst, src, key);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function higherOrderRecursionEntry(dst, src) {
|
||||
higherOrderRecursion(dst, src, (dst, src, key) => {
|
||||
higherOrderRecursionEntry(dst[key], src[key]);
|
||||
});
|
||||
}
|
||||
|
||||
function instanceofObjectGuard(dst, src) {
|
||||
for (let key in src) {
|
||||
let dstValue = dst[key];
|
||||
if (typeof dstValue === 'object' && dstValue instanceof Object) {
|
||||
instanceofObjectGuard(dstValue, src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let blacklist = ["__proto__", "constructor"];
|
||||
|
||||
function copyWithBlacklist(dst, src) {
|
||||
for (let key in src) {
|
||||
if (blacklist.indexOf(key) >= 0) continue;
|
||||
if (dst[key]) {
|
||||
copyWithBlacklist(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyUsingPlainForLoop(dst, src) {
|
||||
let keys = Object.keys(src);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
let key = keys[i];
|
||||
if (dst[key]) {
|
||||
copyUsingPlainForLoop(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyUsingPlainForLoopNoAlias(dst, src) {
|
||||
// Like copyUsingPlainForLoop, but with keys[i] duplicated at every use-site
|
||||
let keys = Object.keys(src);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (dst[key]) {
|
||||
copyUsingPlainForLoopNoAlias(dst[keys[i]], src[keys[i]]);
|
||||
} else {
|
||||
dst[keys[i]] = src[keys[i]]; // NOT OK - but not flagged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deepSet(map, key1, key2, value) {
|
||||
if (!map[key1]) {
|
||||
map[key1] = Object.create(null);
|
||||
}
|
||||
map[key1][key2] = value; // OK
|
||||
}
|
||||
|
||||
function deepSetCaller(data) {
|
||||
let map1 = Object.create(null);
|
||||
let map2 = Object.create(null);
|
||||
for (let key in data) {
|
||||
deepSet(map1, key, 'x', data[key]);
|
||||
deepSet(map2, 'x', key, data[key]);
|
||||
}
|
||||
}
|
||||
|
||||
function deepSetBad(map, key1, key2, value) {
|
||||
if (!map[key1]) {
|
||||
map[key1] = Object.create(null);
|
||||
}
|
||||
map[key1][key2] = value; // NOT OK - object literal can flow here
|
||||
}
|
||||
|
||||
function deepSetCallerBad(data) {
|
||||
let map1 = Object.create(null);
|
||||
for (let key in data) {
|
||||
deepSetBad({}, key, 'x', data[key]); // oops
|
||||
deepSetBad(map1, 'x', key, data[key]);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeCopy(x) {
|
||||
if (x && typeof x === 'object') {
|
||||
return {...x};
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeWithCopy(dst, src) {
|
||||
if (dst == null) return src;
|
||||
let result = maybeCopy(dst);
|
||||
for (let key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
result[key] = mergeWithCopy(dst[key], src[key]); // OK
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function copyUsingEntries(dst, src) {
|
||||
Object.entries(src).forEach(entry => {
|
||||
let key = entry[0];
|
||||
let value = entry[1];
|
||||
if (dst[key]) {
|
||||
copyUsingEntries(dst[key], value);
|
||||
} else {
|
||||
dst[key] = value; // NOT OK
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyUsingReflect(dst, src) {
|
||||
Reflect.ownKeys(src).forEach(key => {
|
||||
if (dst[key]) {
|
||||
copyUsingReflect(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // NOT OK
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyWithPath(dst, src, path) {
|
||||
for (let key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
if (dst[key]) {
|
||||
copyWithPath(dst[key], src[key], path ? path + '.' + key : key);
|
||||
} else {
|
||||
let target = {};
|
||||
target[path] = {};
|
||||
target[path][key] = src[key]; // OK
|
||||
doSomething(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
function typeofObjectTest(dst, src) {
|
||||
for (let key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
let value = src[key];
|
||||
if (dst[key] && typeof value === 'object') {
|
||||
typeofObjectTest(dst[key], value);
|
||||
} else {
|
||||
dst[key] = value; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeRephinementNode(dst, src) {
|
||||
for (let key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
if (key === key && key === key) continue; // Create a phi-node of refinement nodes
|
||||
let value = src[key];
|
||||
if (dst[key] && typeof value === 'object') {
|
||||
mergeRephinementNode(dst[key], value);
|
||||
} else {
|
||||
dst[key] = value; // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeSelective(dst, src) {
|
||||
for (let key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
// Only 'prefs' is merged recursively
|
||||
if (key in dst && key !== 'prefs') {
|
||||
continue;
|
||||
}
|
||||
if (dst[key]) {
|
||||
mergeSelective(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key]; // OK
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isNonArrayObject(item) {
|
||||
return item && typeof item === 'object' && !Array.isArray(item);
|
||||
}
|
||||
|
||||
function mergePlainObjectsOnly(target, source) {
|
||||
if (isNonArrayObject(target) && isNonArrayObject(source)) {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (key === '__proto__') {
|
||||
return;
|
||||
}
|
||||
if (isNonArrayObject(source[key]) && key in target) {
|
||||
target[key] = mergePlainObjectsOnly(target[key], source[key], options);
|
||||
} else {
|
||||
target[key] = source[key]; // OK
|
||||
}
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function merge(dst, src) {
|
||||
for (let key in src) {
|
||||
if (!src.hasOwnProperty(key)) continue;
|
||||
if (isObject(dst[key])) {
|
||||
merge(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function merge(dst, src) {
|
||||
for (let key in src) {
|
||||
if (!src.hasOwnProperty(key)) continue;
|
||||
if (dst.hasOwnProperty(key) && isObject(dst[key])) {
|
||||
merge(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
function merge(dst, src) {
|
||||
for (let key in src) {
|
||||
if (!src.hasOwnProperty(key)) continue;
|
||||
if (key === "__proto__" || key === "constructor") continue;
|
||||
if (isObject(dst[key])) {
|
||||
merge(dst[key], src[key]);
|
||||
} else {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user