mirror of
https://github.com/github/codeql.git
synced 2026-04-26 09:15:12 +02:00
Merge pull request #4778 from asgerf/js/more-prototype-pollution
Approved by erik-krogh, mchammer01
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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)
|
||||
|
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user