JS: Add query for resource exhaustion from deep object handling

This commit is contained in:
Asger Feldthaus
2021-02-24 15:33:15 +00:00
parent b978359803
commit 24199a5499
6 changed files with 227 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Processing user-controlled data with a method that allocates excessive amounts
of memory can lead to denial of service.
</p>
<p>
If the JSON schema validation library <code>ajv</code> is configured with
<code>allErrors: true</code> there is no limit to how many error objects
will be allocated. An attacker can exploit this by sending an object that
deliberately contains a huge number of errors, and in some cases, with
longer and longer error messages. This can cause the service to become
unresponsive due to the slow error-checking process.
</p>
</overview>
<recommendation>
<p>
Do not use <code>allErrors: true</code> in production.
</p>
</recommendation>
<example>
<p>
In the example below, the user-submitted object <code>req.body</code> is
validated using <code>ajv</code> and <code>allErrors: true</code>:
</p>
<sample src="examples/DeepObjectResourceExhaustion.js"/>
<p>
Although this ensures that <code>req.body</code> conforms to the schema,
the validation itself could be vulnerable to a denial-of-service attack.
An attacker could send an object containing so many errors that the server
runs out of memory.
</p>
<p>
A solution is to not pass in <code>allErrors: true</code>, which means
<code>ajv</code> will only report the first error, not all of them:
</p>
<sample src="examples/DeepObjectResourceExhaustion_fixed.js"/>
</example>
<references>
<li>Ajv documentation: <a href="https://github.com/ajv-validator/ajv/blob/master/docs/security.md#untrusted-schemas">security considerations</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,23 @@
/**
* @name Resources exhaustion from deep object traversal
* @description Processing user-controlled object hierarchies inefficiently can lead to denial of service.
* @kind path-problem
* @problem.severity warning
* @precision high
* @id js/resource-exhaustion-from-deep-object-traversal
* @tags security
* external/cwe/cwe-400
*/
import javascript
import DataFlow::PathGraph
import semmle.javascript.security.dataflow.DeepObjectResourceExhaustion::DeepObjectResourceExhaustion
from
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node link,
string reason
where
cfg.hasFlowPath(source, sink) and
sink.getNode().(Sink).hasReason(link, reason)
select sink, source, sink, "Denial of service caused by processing user input from $@ with $@.",
source.getNode(), "here", link, reason

View File

@@ -0,0 +1,14 @@
import express from 'express';
import Ajv from 'ajv';
let ajv = new Ajv({ allErrors: true });
ajv.addSchema(require('./input-schema'), 'input');
var app = express();
app.get('/user/:id', function(req, res) {
if (!ajv.validate('input', req.body)) {
res.end(ajv.errorsText());
return;
}
// ...
});

View File

@@ -0,0 +1,14 @@
import express from 'express';
import Ajv from 'ajv';
let ajv = new Ajv({ allErrors: process.env['REST_DEBUG'] });
ajv.addSchema(require('./input-schema'), 'input');
var app = express();
app.get('/user/:id', function(req, res) {
if (!ajv.validate('input', req.body)) {
res.end(ajv.errorsText());
return;
}
// ...
});

View File

@@ -0,0 +1,43 @@
/**
* Provides a taint tracking configuration for reasoning about DoS attacks
* due to inefficient handling of user-controlled objects.
*/
import javascript
import semmle.javascript.security.TaintedObject
module DeepObjectResourceExhaustion {
import DeepObjectResourceExhaustionCustomizations::DeepObjectResourceExhaustion
/**
* A taint tracking configuration for reasoning about DoS attacks due to inefficient handling
* of user-controlled objects.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "DeepObjectResourceExhaustion" }
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
source instanceof Source and label = TaintedObject::label()
or
// We currently can't expose the TaintedObject label in the Customizations library
// so just add its default sources here.
source instanceof TaintedObject::Source and label = TaintedObject::label()
or
source instanceof RemoteFlowSource and label.isTaint()
}
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
sink instanceof Sink and label = TaintedObject::label()
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof TaintedObject::SanitizerGuard
}
override predicate isAdditionalFlowStep(
DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl
) {
TaintedObject::step(src, trg, inlbl, outlbl)
}
}
}

View File

@@ -0,0 +1,78 @@
/**
* Provides sources, sinks and sanitizers for reasoning about
* DoS attacks due to inefficient handling of user-controlled objects.
*/
import javascript
/**
* Provides sources, sinks and sanitizers for reasoning about
* DoS attacks due to inefficient handling of user-controlled objects.
*/
module DeepObjectResourceExhaustion {
/**
* A data flow source for slow input validation.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for slow input validation.
*/
abstract class Sink extends DataFlow::Node {
/**
* Holds if `link` and `text` should be included in the message to explain
* why the input validation is slow.
*/
abstract predicate hasReason(DataFlow::Node link, string text);
}
/**
* A sanitizer for slow input validation.
*/
abstract class Sanitizer extends DataFlow::Node { }
/** Gets a node that may refer to an object with `allErrors` set to `true`. */
private DataFlow::SourceNode allErrorsObject(
DataFlow::TypeTracker t, DataFlow::PropWrite allErrors
) {
t.start() and
exists(JsonSchema::Ajv::AjvValidationCall call) and // only compute if `ajv` is used
allErrors.getPropertyName() = "allErrors" and
allErrors.getRhs().mayHaveBooleanValue(true) and
result = allErrors.getBase().getALocalSource()
or
exists(ExtendCall call |
allErrorsObject(t.continue(), allErrors).flowsTo(call.getAnOperand()) and
(result = call or result = call.getDestinationOperand().getALocalSource())
)
or
exists(DataFlow::ObjectLiteralNode obj |
allErrorsObject(t.continue(), allErrors).flowsTo(obj.getASpreadProperty()) and
result = obj
)
or
exists(DataFlow::TypeTracker t2 | result = allErrorsObject(t2, allErrors).track(t2, t))
}
/** Gets a node that may refer to an object with `allErrors` set to `true`. */
private DataFlow::SourceNode allErrorsObject(DataFlow::PropWrite allErrors) {
result = allErrorsObject(DataFlow::TypeTracker::end(), allErrors)
}
/** Argument to an `ajv` validation call configured with `allErrors: true`. */
private class AjvValidationSink extends Sink {
DataFlow::PropWrite allErrors;
AjvValidationSink() {
exists(JsonSchema::Ajv::AjvValidationCall call |
this = call.getInput() and
allErrorsObject(allErrors).flowsTo(call.getAnOptionsArg())
)
}
override predicate hasReason(DataFlow::Node link, string text) {
link = allErrors and
text = "allErrors: true"
}
}
}