JS: Add PrototypePollutionUtility query

This commit is contained in:
Asger F
2019-10-31 12:59:07 +00:00
committed by Asger Feldthaus
parent 52cec25035
commit 654f145772
9 changed files with 1950 additions and 0 deletions

View File

@@ -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__": {"xxx": true}}</code>,
it will inject the property <code>xxx: true</code> in 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>

View File

@@ -0,0 +1,365 @@
/**
* @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-079
* external/cwe/cwe-116
*/
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.
*/
class EnumeratedPropName extends DataFlow::Node {
DataFlow::Node object;
EnumeratedPropName() {
exists(ForInStmt stmt |
this = DataFlow::lvalueNode(stmt.getLValue()) and
object = stmt.getIterationDomain().flow()
)
or
exists(CallNode call, string name |
call = globalVarRef("Object").getAMemberCall(name) and
(name = "keys" or name = "getOwnPropertyNames") and
object = call.getArgument(0) and
this = getAnEnumeratedArrayElement(call)
)
}
/**
* Gets the object whose properties are being enumerated.
*
* For example, gets `src` in `for (var key in src)`.
*/
Node getSourceObject() { result = object }
/**
* 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
}
}
/**
* 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 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 four separate data flow paths originating
* from a property enumeration, all leading to the same dynamic property write.
*
* In particular, the base, property name, and rhs of the property write should all
* depend on the enumerated property name (`key`) and the right-hand side should
* additionally depend on the source object (`src`), 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.
*/
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
succ = read
)
or
// Step through `x -> x[p]`
exists(DynamicPropRead read |
pred = read.getBase() and
succ = read
)
)
}
override predicate isBarrierGuard(DataFlow::BarrierGuardNode node) {
node instanceof EqualityGuard or
node instanceof HasOwnPropertyGuard or
node instanceof InstanceOfGuard or
node instanceof TypeofGuard or
node instanceof ArrayInclusionGuard
}
}
/**
* Sanitizer guard of form `x === "__proto__"` or `x === "constructor"`.
*/
class EqualityGuard extends DataFlow::LabeledBarrierGuardNode, ValueNode {
override EqualityTest astNode;
string propName;
EqualityGuard() {
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
}
}
/**
* 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 `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__"
)
}
}
/**
* A check of form `["__proto__"].includes(x)` or similar.
*/
class ArrayInclusionGuard extends DataFlow::LabeledBarrierGuardNode, InclusionTest {
UnsafePropLabel label;
ArrayInclusionGuard() {
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
}
}
/**
* 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"
}
from
PropNameTracking cfg, DataFlow::PathNode source, DataFlow::PathNode sink, EnumeratedPropName enum,
Node base, Node prop, Node rhs
where
cfg.hasFlowPath(source, sink) and
dynamicPropWrite(base, prop, rhs) and
sink.getNode() = base and
source.getNode() = enum and
cfg.hasFlow(enum, prop) and
cfg.hasFlow(enum, rhs) and
cfg.hasFlow(enum.getASourceProp(), rhs)
select base, source, sink,
"Properties are copied from $@ to $@ without guarding against prototype pollution.",
enum.getSourceObject(), deriveExprName(enum.getSourceObject()), base, deriveExprName(base)

View File

@@ -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];
}
}
}

View File

@@ -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];
}
}
}

View File

@@ -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];
}
}
}