Merge pull request #4778 from asgerf/js/more-prototype-pollution

Approved by erik-krogh, mchammer01
This commit is contained in:
CodeQL CI
2020-12-11 13:58:09 -08:00
committed by GitHub
49 changed files with 3693 additions and 3079 deletions

View File

@@ -15,5 +15,6 @@ import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Potential type confusion for $@.", source.getNode(),
"HTTP request parameter"
select sink.getNode(), source, sink,
"Potential type confusion as $@ may be either an array or a string.", source.getNode(),
"this HTTP request parameter"

View File

@@ -0,0 +1,62 @@
<!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> object, 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 by modifying an object obtained via a user-controlled property name.
Most objects have a special <code>__proto__</code> property that refers to <code>Object.prototype</code>.
An attacker can abuse this special property to trick the application into performing unintended modifications
of <code>Object.prototype</code>.
</p>
</overview>
<recommendation>
<p>
Use an associative data structure that is resilient to untrusted key values, such as a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">Map</a>.
In some cases, a prototype-less object created with <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create">Object.create(null)</a>
may be preferable.
</p>
<p>
Alternatively, restrict the computed property name so it can't clash with a built-in property, either by
prefixing it with a constant string, or by rejecting inputs that don't conform to the expected format.
</p>
</recommendation>
<example>
<p>
In the example below, the untrusted value <code>req.params.id</code> is used as the property name
<code>req.session.todos[id]</code>. If a malicious user passes in the ID value <code>__proto__</code>,
the variable <code>todo</code> will then refer to <code>Object.prototype</code>.
Finally, the modification of <code>todo</code> then allows the attacker to inject arbitrary properties
onto <code>Object.prototype</code>.
</p>
<sample src="examples/PrototypePollutingAssignment.js"/>
<p>
One way to fix this is to use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">Map</a> objects to associate key/value pairs
instead of regular objects, as shown below:
</p>
<sample src="examples/PrototypePollutingAssignmentFixed.js"/>
</example>
<references>
<li>MDN:
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto">Object.prototype.__proto__</a>
</li>
<li>MDN:
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">Map</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,26 @@
/**
* @name Prototype-polluting assignment
* @description Modifying an object obtained via a user-controlled property name may
* lead to accidental mutation of the built-in Object prototype,
* and possibly escalate to remote code execution or cross-site scripting.
* @kind path-problem
* @problem.severity warning
* @precision high
* @id js/prototype-polluting-assignment
* @tags security
* external/cwe/cwe-078
* external/cwe/cwe-079
* external/cwe/cwe-094
* external/cwe/cwe-400
* external/cwe/cwe-915
*/
import javascript
import semmle.javascript.security.dataflow.PrototypePollutingAssignment::PrototypePollutingAssignment
import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink,
"This assignment may alter Object.prototype if a malicious '__proto__' string is injected from $@.",
source.getNode(), "here"

View File

@@ -29,7 +29,7 @@
<p>
Only merge or assign 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>
Alternatively, block the property names <code>__proto__</code> and <code>constructor</code>
from being merged or assigned to.
</p>
</recommendation>
@@ -39,7 +39,7 @@
This function recursively copies properties from <code>src</code> to <code>dst</code>:
</p>
<sample src="examples/PrototypePollutionUtility.js"/>
<sample src="examples/PrototypePollutingFunction.js"/>
<p>
However, if <code>src</code> is the object <code>{"__proto__": {"isAdmin": true}}</code>,
@@ -51,13 +51,13 @@
are merged recursively:
</p>
<sample src="examples/PrototypePollutionUtility_fixed.js"/>
<sample src="examples/PrototypePollutingFunction_fixed.js"/>
<p>
Alternatively, blacklist the <code>__proto__</code> and <code>constructor</code> properties:
Alternatively, block the <code>__proto__</code> and <code>constructor</code> properties:
</p>
<sample src="examples/PrototypePollutionUtility_fixed2.js"/>
<sample src="examples/PrototypePollutingFunction_fixed2.js"/>
</example>
<references>

View File

@@ -1,14 +1,17 @@
/**
* @name Prototype pollution in utility function
* @description Recursively assigning properties on objects may cause
* accidental modification of a built-in prototype object.
* @name Prototype-polluting function
* @description Functions recursively assigning properties on objects may be
* the cause of 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-078
* external/cwe/cwe-079
* external/cwe/cwe-094
* external/cwe/cwe-400
* external/cwe/cwe-471
* external/cwe/cwe-915
*/
import javascript
@@ -276,14 +279,14 @@ class PropNameTracking extends DataFlow::Configuration {
}
override predicate isBarrierGuard(DataFlow::BarrierGuardNode node) {
node instanceof BlacklistEqualityGuard or
node instanceof WhitelistEqualityGuard or
node instanceof DenyListEqualityGuard or
node instanceof AllowListEqualityGuard or
node instanceof HasOwnPropertyGuard or
node instanceof InExprGuard or
node instanceof InstanceOfGuard or
node instanceof TypeofGuard or
node instanceof BlacklistInclusionGuard or
node instanceof WhitelistInclusionGuard or
node instanceof DenyListInclusionGuard or
node instanceof AllowListInclusionGuard or
node instanceof IsPlainObjectGuard
}
}
@@ -291,11 +294,11 @@ class PropNameTracking extends DataFlow::Configuration {
/**
* Sanitizer guard of form `x === "__proto__"` or `x === "constructor"`.
*/
class BlacklistEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
class DenyListEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
override EqualityTest astNode;
string propName;
BlacklistEqualityGuard() {
DenyListEqualityGuard() {
astNode.getAnOperand().getStringValue() = propName and
propName = unsafePropName()
}
@@ -310,10 +313,10 @@ class BlacklistEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNod
/**
* An equality test with something other than `__proto__` or `constructor`.
*/
class WhitelistEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
class AllowListEqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
override EqualityTest astNode;
WhitelistEqualityGuard() {
AllowListEqualityGuard() {
not astNode.getAnOperand().getStringValue() = unsafePropName() and
astNode.getAnOperand() instanceof Literal
}
@@ -427,10 +430,10 @@ class TypeofGuard extends DataFlow::LabeledBarrierGuardNode, DataFlow::ValueNode
/**
* A check of form `["__proto__"].includes(x)` or similar.
*/
class BlacklistInclusionGuard extends DataFlow::LabeledBarrierGuardNode, InclusionTest {
class DenyListInclusionGuard extends DataFlow::LabeledBarrierGuardNode, InclusionTest {
UnsafePropLabel label;
BlacklistInclusionGuard() {
DenyListInclusionGuard() {
exists(DataFlow::ArrayCreationNode array |
array.getAnElement().getStringValue() = label and
array.flowsTo(getContainerNode())
@@ -447,8 +450,8 @@ class BlacklistInclusionGuard extends DataFlow::LabeledBarrierGuardNode, Inclusi
/**
* A check of form `xs.includes(x)` or similar, which sanitizes `x` in the true case.
*/
class WhitelistInclusionGuard extends DataFlow::LabeledBarrierGuardNode {
WhitelistInclusionGuard() {
class AllowListInclusionGuard extends DataFlow::LabeledBarrierGuardNode {
AllowListInclusionGuard() {
this instanceof TaintTracking::PositiveIndexOfSanitizer
or
this instanceof TaintTracking::MembershipTestSanitizer and

View File

@@ -34,7 +34,7 @@
and then copied into a new object:
</p>
<sample src="examples/PrototypePollution1.js"/>
<sample src="examples/PrototypePollutingMergeCall1.js"/>
<p>
Prior to lodash 4.17.11 this would be vulnerable to prototype pollution. An attacker could send the following GET request:
@@ -47,7 +47,7 @@
Fix this by updating the lodash version:
</p>
<sample src="examples/PrototypePollution_fixed.json"/>
<sample src="examples/PrototypePollutingMergeCall_fixed.json"/>
<p>
Note that some web frameworks, such as Express, parse query parameters using extended URL-encoding
@@ -56,7 +56,7 @@
The example below would also be susceptible to prototype pollution:
</p>
<sample src="examples/PrototypePollution2.js"/>
<sample src="examples/PrototypePollutingMergeCall2.js"/>
<p>
In the above example, an attacker can cause prototype pollution by sending the following GET request:

View File

@@ -1,14 +1,18 @@
/**
* @name Prototype pollution
* @name Prototype-polluting merge call
* @description Recursively merging a user-controlled object into another object
* can allow an attacker to modify the built-in Object prototype.
* can allow an attacker to modify the built-in Object prototype,
* and possibly escalate to remote code execution or cross-site scripting.
* @kind path-problem
* @problem.severity error
* @precision high
* @id js/prototype-pollution
* @tags security
* external/cwe/cwe-250
* external/cwe/cwe-078
* external/cwe/cwe-079
* external/cwe/cwe-094
* external/cwe/cwe-400
* external/cwe/cwe-915
*/
import javascript

View File

@@ -0,0 +1,11 @@
let express = require('express');
express.put('/todos/:id', (req, res) => {
let id = req.params.id;
let items = req.session.todos[id];
if (!items) {
items = req.session.todos[id] = {};
}
items[req.query.name] = req.query.text;
res.end(200);
});

View File

@@ -0,0 +1,12 @@
let express = require('express');
express.put('/todos/:id', (req, res) => {
let id = req.params.id;
let items = req.session.todos.get(id);
if (!items) {
items = new Map();
req.sessions.todos.set(id, items);
}
items.set(req.query.name, req.query.text);
res.end(200);
});

View File

@@ -214,6 +214,7 @@ abstract class Configuration extends string {
* Holds if `guard` is a barrier guard for this configuration, added through
* `isBarrierGuard` or `AdditionalBarrierGuardNode`.
*/
pragma[nomagic]
private predicate isBarrierGuardInternal(BarrierGuardNode guard) {
isBarrierGuard(guard)
or
@@ -368,6 +369,7 @@ abstract class BarrierGuardNode extends DataFlow::Node {
*
* `label` is bound to the blocked label, or the empty string if all labels should be blocked.
*/
pragma[nomagic]
private predicate barrierGuardBlocksExpr(
BarrierGuardNode guard, boolean outcome, Expr test, string label
) {
@@ -383,7 +385,7 @@ private predicate barrierGuardBlocksExpr(
/**
* Holds if `guard` may block the flow of a value reachable through exploratory flow.
*/
pragma[noinline]
pragma[nomagic]
private predicate barrierGuardIsRelevant(BarrierGuardNode guard) {
exists(Expr e |
barrierGuardBlocksExpr(guard, _, e, _) and
@@ -397,7 +399,7 @@ private predicate barrierGuardIsRelevant(BarrierGuardNode guard) {
*
* `label` is bound to the blocked label, or the empty string if all labels should be blocked.
*/
pragma[noinline]
pragma[nomagic]
private predicate barrierGuardBlocksAccessPath(
BarrierGuardNode guard, boolean outcome, AccessPath ap, string label
) {
@@ -410,6 +412,7 @@ private predicate barrierGuardBlocksAccessPath(
*
* This predicate is outlined to give the optimizer a hint about the join ordering.
*/
pragma[nomagic]
private predicate barrierGuardBlocksSsaRefinement(
BarrierGuardNode guard, boolean outcome, SsaRefinementNode ref, string label
) {
@@ -425,7 +428,7 @@ private predicate barrierGuardBlocksSsaRefinement(
*
* `outcome` is bound to the outcome of `cond` for join-ordering purposes.
*/
pragma[noinline]
pragma[nomagic]
private predicate barrierGuardUsedInCondition(
BarrierGuardNode guard, ConditionGuardNode cond, boolean outcome
) {
@@ -444,6 +447,7 @@ private predicate barrierGuardUsedInCondition(
*
* `label` is bound to the blocked label, or the empty string if all labels should be blocked.
*/
pragma[nomagic]
private predicate barrierGuardBlocksNode(BarrierGuardNode guard, DataFlow::Node nd, string label) {
// 1) `nd` is a use of a refinement node that blocks its input variable
exists(SsaRefinementNode ref, boolean outcome |
@@ -466,6 +470,7 @@ private predicate barrierGuardBlocksNode(BarrierGuardNode guard, DataFlow::Node
*
* `label` is bound to the blocked label, or the empty string if all labels should be blocked.
*/
pragma[nomagic]
private predicate barrierGuardBlocksEdge(
BarrierGuardNode guard, DataFlow::Node pred, DataFlow::Node succ, string label
) {
@@ -1183,50 +1188,50 @@ private predicate loadStep(
}
/**
* Holds if `rhs` is the right-hand side of a write to property `prop`, and `nd` is reachable
* from the base of that write under configuration `cfg` (possibly through callees) along a
* path summarized by `summary`.
* Holds if there is flow to `base.startProp`, and `base.startProp` flows to `nd.endProp` under `cfg/summary`.
*/
pragma[nomagic]
private predicate reachableFromStoreBase(
string prop, DataFlow::Node rhs, DataFlow::Node nd, DataFlow::Configuration cfg,
PathSummary summary
string startProp, string endProp, DataFlow::Node base, DataFlow::Node nd,
DataFlow::Configuration cfg, PathSummary summary
) {
exists(PathSummary s1, PathSummary s2 |
exists(PathSummary s1, PathSummary s2, DataFlow::Node rhs |
reachableFromSource(rhs, cfg, s1)
or
reachableFromStoreBase(_, _, rhs, cfg, s1)
reachableFromStoreBase(_, _, _, rhs, cfg, s1)
|
storeStep(rhs, nd, prop, cfg, s2) and
storeStep(rhs, nd, startProp, cfg, s2) and
endProp = startProp and
base = nd and
summary =
MkPathSummary(false, s1.hasCall().booleanOr(s2.hasCall()), s2.getStartLabel(),
s2.getEndLabel())
MkPathSummary(false, s1.hasCall().booleanOr(s2.hasCall()), DataFlow::FlowLabel::data(),
DataFlow::FlowLabel::data())
)
or
exists(PathSummary newSummary, PathSummary oldSummary |
reachableFromStoreBaseStep(prop, rhs, nd, cfg, oldSummary, newSummary) and
reachableFromStoreBaseStep(startProp, endProp, base, nd, cfg, oldSummary, newSummary) and
summary = oldSummary.appendValuePreserving(newSummary)
)
}
/**
* Holds if `rhs` is the right-hand side of a write to property `prop`, and `nd` is reachable
* from the base of that write under configuration `cfg` (possibly through callees) along a
* path whose last step is summarized by `newSummary`, and the previous steps are summarized
* Holds if `base` is the base of a write to property `prop`, and `nd` is reachable
* from `base` under configuration `cfg` (possibly through callees) along a path whose
* last step is summarized by `newSummary`, and the previous steps are summarized
* by `oldSummary`.
*/
pragma[noinline]
private predicate reachableFromStoreBaseStep(
string prop, DataFlow::Node rhs, DataFlow::Node nd, DataFlow::Configuration cfg,
PathSummary oldSummary, PathSummary newSummary
string startProp, string endProp, DataFlow::Node base, DataFlow::Node nd,
DataFlow::Configuration cfg, PathSummary oldSummary, PathSummary newSummary
) {
exists(DataFlow::Node mid |
reachableFromStoreBase(prop, rhs, mid, cfg, oldSummary) and
reachableFromStoreBase(startProp, endProp, base, mid, cfg, oldSummary) and
flowStep(mid, cfg, nd, newSummary)
or
exists(string midProp |
reachableFromStoreBase(midProp, rhs, mid, cfg, oldSummary) and
isAdditionalLoadStoreStep(mid, nd, midProp, prop, cfg) and
reachableFromStoreBase(startProp, midProp, base, mid, cfg, oldSummary) and
isAdditionalLoadStoreStep(mid, nd, midProp, endProp, cfg) and
newSummary = PathSummary::level()
)
)
@@ -1260,9 +1265,14 @@ private predicate storeToLoad(
DataFlow::Node pred, DataFlow::Node succ, DataFlow::Configuration cfg, PathSummary oldSummary,
PathSummary newSummary
) {
exists(string prop, DataFlow::Node base |
reachableFromStoreBase(prop, pred, base, cfg, oldSummary) and
loadStep(base, succ, prop, cfg, newSummary)
exists(
string storeProp, string loadProp, DataFlow::Node storeBase, DataFlow::Node loadBase,
PathSummary s1, PathSummary s2
|
storeStep(pred, storeBase, storeProp, cfg, s1) and
reachableFromStoreBase(storeProp, loadProp, storeBase, loadBase, cfg, s2) and
oldSummary = s1.appendValuePreserving(s2) and
loadStep(loadBase, succ, loadProp, cfg, newSummary)
)
}
@@ -1281,6 +1291,9 @@ private predicate summarizedHigherOrderCall(
DataFlow::Node innerArg, DataFlow::SourceNode cbParm, PathSummary oldSummary
|
reachableFromInput(f, outer, arg, innerArg, cfg, oldSummary) and
// Only track actual parameter flow.
// Captured flow does not need to be summarized - it is handled by the local case in `higherOrderCall`.
not arg = DataFlow::capturedVariableNode(_) and
argumentPassing(outer, cb, f, cbParm) and
innerArg = inner.getArgument(j)
|

View File

@@ -63,6 +63,14 @@ module TaintedObject {
src = call.getASourceOperand() and
trg = call.getDestinationOperand().getALocalSource()
)
or
// Spreading into an object preserves deep object taint: `p -> { ...p }`
inlbl = label() and
outlbl = label() and
exists(ObjectLiteralNode obj |
src = obj.getASpreadProperty() and
trg = obj
)
}
/**

View File

@@ -0,0 +1,212 @@
/**
* Provides a taint tracking configuration for reasoning about
* prototype-polluting assignments.
*
* Note, for performance reasons: only import this file if
* `PrototypePollutingAssignment::Configuration` is needed, otherwise
* `PrototypePollutingAssignmentCustomizations` should be imported instead.
*/
private import javascript
private import semmle.javascript.DynamicPropertyAccess
/**
* Provides a taint tracking configuration for reasoning about
* prototype-polluting assignments.
*/
module PrototypePollutingAssignment {
private import PrototypePollutingAssignmentCustomizations::PrototypePollutingAssignment
// Materialize flow labels
private class ConcreteObjectPrototype extends ObjectPrototype {
ConcreteObjectPrototype() { this = this }
}
/** A taint-tracking configuration for reasoning about prototype-polluting assignments. */
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "PrototypePollutingAssignment" }
override predicate isSource(DataFlow::Node node) { node instanceof Source }
override predicate isSink(DataFlow::Node node, DataFlow::FlowLabel lbl) {
node.(Sink).getAFlowLabel() = lbl
}
override predicate isSanitizer(DataFlow::Node node) {
node instanceof Sanitizer
or
// Concatenating with a string will in practice prevent the string `__proto__` from arising.
node instanceof StringOps::ConcatenationRoot
}
override predicate isAdditionalFlowStep(
DataFlow::Node pred, DataFlow::Node succ, DataFlow::FlowLabel inlbl,
DataFlow::FlowLabel outlbl
) {
// Step from x -> obj[x] while switching to the ObjectPrototype label
// (If `x` can have the value `__proto__` then the result can be Object.prototype)
exists(DynamicPropRead read |
pred = read.getPropertyNameNode() and
succ = read and
inlbl.isTaint() and
outlbl instanceof ObjectPrototype and
// Exclude cases where the property name came from a property enumeration.
// If the property name is an own property of the base object, the read won't
// return Object.prototype.
not read = any(EnumeratedPropName n).getASourceProp() and
// Exclude cases where the read has no prototype, or a prototype other than Object.prototype.
not read = prototypeLessObject().getAPropertyRead() and
// Exclude cases where this property has just been assigned to
not read.hasDominatingAssignment()
)
or
// Same as above, but for property projection.
exists(PropertyProjection proj |
proj.isSingletonProjection() and
pred = proj.getASelector() and
succ = proj and
inlbl.isTaint() and
outlbl instanceof ObjectPrototype
)
}
override predicate isLabeledBarrier(DataFlow::Node node, DataFlow::FlowLabel lbl) {
super.isLabeledBarrier(node, lbl)
or
// Don't propagate into the receiver, as the method lookups will generally fail on Object.prototype.
node instanceof DataFlow::ThisNode and
lbl instanceof ObjectPrototype
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof PropertyPresenceCheck or
guard instanceof InExprCheck or
guard instanceof InstanceofCheck or
guard instanceof IsArrayCheck or
guard instanceof TypeofCheck or
guard instanceof EqualityCheck
}
}
/** Gets a data flow node referring to an object created with `Object.create`. */
DataFlow::SourceNode prototypeLessObject() {
result = prototypeLessObject(DataFlow::TypeTracker::end())
}
private DataFlow::SourceNode prototypeLessObject(DataFlow::TypeTracker t) {
t.start() and
// We assume the argument to Object.create is not Object.prototype, since most
// users wouldn't bother to call Object.create in that case.
result = DataFlow::globalVarRef("Object").getAMemberCall("create")
or
// Allow use of AdditionalFlowSteps and AdditionalTaintSteps to track a bit further
exists(DataFlow::Node mid |
prototypeLessObject(t.continue()).flowsTo(mid) and
any(DataFlow::AdditionalFlowStep s).step(mid, result)
)
or
exists(DataFlow::TypeTracker t2 | result = prototypeLessObject(t2).track(t2, t))
}
/** Holds if `Object.prototype` has a member named `prop`. */
private predicate isPropertyPresentOnObjectPrototype(string prop) {
exists(ExternalInstanceMemberDecl decl |
decl.getBaseName() = "Object" and
decl.getName() = prop
)
}
/** A check of form `e.prop` where `prop` is not present on `Object.prototype`. */
private class PropertyPresenceCheck extends TaintTracking::LabeledSanitizerGuardNode,
DataFlow::ValueNode {
override PropAccess astNode;
PropertyPresenceCheck() {
astNode = any(ConditionGuardNode c).getTest() and // restrict size of charpred
not isPropertyPresentOnObjectPrototype(astNode.getPropertyName())
}
override predicate sanitizes(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = astNode.getBase() and
outcome = true and
label instanceof ObjectPrototype
}
}
/** A check of form `"prop" in e` where `prop` is not present on `Object.prototype`. */
private class InExprCheck extends TaintTracking::LabeledSanitizerGuardNode, DataFlow::ValueNode {
override InExpr astNode;
InExprCheck() {
not isPropertyPresentOnObjectPrototype(astNode.getLeftOperand().getStringValue())
}
override predicate sanitizes(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = astNode.getRightOperand() and
outcome = true and
label instanceof ObjectPrototype
}
}
/** A check of form `e instanceof X`, which is always false for `Object.prototype`. */
private class InstanceofCheck extends TaintTracking::LabeledSanitizerGuardNode,
DataFlow::ValueNode {
override InstanceofExpr astNode;
override predicate sanitizes(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = astNode.getLeftOperand() and
outcome = true and
label instanceof ObjectPrototype
}
}
/** A check of form `typeof e === "string"`. */
private class TypeofCheck extends TaintTracking::LabeledSanitizerGuardNode, DataFlow::ValueNode {
override EqualityTest astNode;
Expr operand;
string value;
TypeofCheck() {
exists(TypeofExpr typeof, Expr str |
astNode.hasOperands(typeof, str) and
typeof.getOperand() = operand and
str.getStringValue() = value
)
}
override predicate sanitizes(boolean outcome, Expr e, DataFlow::FlowLabel label) {
(
value = "object" and outcome = astNode.getPolarity().booleanNot()
or
value != "object" and outcome = astNode.getPolarity()
) and
e = operand and
label instanceof ObjectPrototype
}
}
/** A call to `Array.isArray`, which is false for `Object.prototype`. */
private class IsArrayCheck extends TaintTracking::LabeledSanitizerGuardNode, DataFlow::CallNode {
IsArrayCheck() { this = DataFlow::globalVarRef("Array").getAMemberCall("isArray") }
override predicate sanitizes(boolean outcome, Expr e, DataFlow::FlowLabel label) {
e = getArgument(0).asExpr() and
outcome = true and
label instanceof ObjectPrototype
}
}
/**
* Sanitizer guard of form `x !== "__proto__"`.
*/
private class EqualityCheck extends TaintTracking::SanitizerGuardNode, DataFlow::ValueNode {
override EqualityTest astNode;
EqualityCheck() { astNode.getAnOperand().getStringValue() = "__proto__" }
override predicate sanitizes(boolean outcome, Expr e) {
e = astNode.getAnOperand() and
outcome = astNode.getPolarity().booleanNot()
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* Provides sources, sinks, and sanitizers for reasoning about assignments
* that my cause prototype pollution.
*/
private import javascript
/**
* Provides sources, sinks, and sanitizers for reasoning about assignments
* that my cause prototype pollution.
*/
module PrototypePollutingAssignment {
/**
* A data flow source for untrusted data from which the special `__proto__` property name may be arise.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for prototype-polluting assignments or untrusted property names.
*/
abstract class Sink extends DataFlow::Node {
/**
* The flow label relevant for this sink.
*
* Use the `taint` label for untrusted property names, and the `ObjectPrototype` label for
* object mutations.
*/
abstract DataFlow::FlowLabel getAFlowLabel();
}
/**
* A sanitizer for untrusted property names.
*/
abstract class Sanitizer extends DataFlow::Node { }
/** Flow label representing the `Object.prototype` value. */
abstract class ObjectPrototype extends DataFlow::FlowLabel {
ObjectPrototype() { this = "Object.prototype" }
}
/** The base of an assignment or extend call, as a sink for `Object.prototype` references. */
private class DefaultSink extends Sink {
DefaultSink() {
this = any(DataFlow::PropWrite write).getBase()
or
this = any(ExtendCall c).getDestinationOperand()
}
override DataFlow::FlowLabel getAFlowLabel() { result instanceof ObjectPrototype }
}
/** A remote flow source or location.{hash,search} as a taint source. */
private class DefaultSource extends Source {
DefaultSource() {
this instanceof RemoteFlowSource
or
this = DOM::locationRef().getAPropertyRead(["hash", "search"])
}
}
}

View File

@@ -92,4 +92,18 @@ module TypeConfusionThroughParameterTampering {
)
}
}
/**
* A value compared to the string `__proto__` or `constructor`, which may be bypassed by wrapping
* the payload in an array.
*/
private class ProtoStringComparison extends Sink {
ProtoStringComparison() {
exists(EqualityTest test |
test.hasOperands(this.asExpr(),
any(Expr e | e.getStringValue() = ["__proto__", "constructor"])) and
test.isStrict()
)
}
}
}