JS: Add PrototypePollutingAssignment

This commit is contained in:
Asger Feldthaus
2020-11-23 17:28:58 +00:00
parent ef52c46aed
commit 972c4d61e5
8 changed files with 345 additions and 0 deletions

View File

@@ -39,6 +39,7 @@
+ semmlecode-javascript-queries/Security/CWE-352/MissingCsrfMiddleware.ql: /Security/CWE/CWE-352
+ 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-471/PrototypePollutingAssignment.ql: /Security/CWE/CWE-471
+ semmlecode-javascript-queries/Security/CWE-400/RemotePropertyInjection.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

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>, 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 preferrable.
</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,23 @@
/**
* @name Prototype-polluting assignment
* @description Modifying an object obtained via a user-controlled property name may
* lead to accidental modification 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-250
* external/cwe/cwe-471
*/
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

@@ -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

@@ -1281,6 +1281,7 @@ private predicate summarizedHigherOrderCall(
DataFlow::Node innerArg, DataFlow::SourceNode cbParm, PathSummary oldSummary
|
reachableFromInput(f, outer, arg, innerArg, cfg, oldSummary) and
not arg = DataFlow::capturedVariableNode(_) and // Only track actual parameter flow
argumentPassing(outer, cb, f, cbParm) and
innerArg = inner.getArgument(j)
|

View File

@@ -0,0 +1,175 @@
/**
* 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
module PrototypePollutingAssignment {
private import PrototypePollutingAssignmentCustomizations::PrototypePollutingAssignment
// Materialize flow labels
private class ConcreteObjectPrototype extends ObjectPrototype { }
/** 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(DataFlow::PropRead read |
pred = read.getPropertyNameExpr().flow() 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.(DynamicPropRead).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) {
// Don't propagate the receiver into method calls, as the method lookup will fail on Object.prototype.
node = any(DataFlow::MethodCallNode m).getReceiver() 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 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() { 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 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"])
}
}
}