diff --git a/javascript/ql/src/Security/CWE-400/DeepObjectResourceExhaustion.qhelp b/javascript/ql/src/Security/CWE-400/DeepObjectResourceExhaustion.qhelp new file mode 100644 index 00000000000..e778d9237d5 --- /dev/null +++ b/javascript/ql/src/Security/CWE-400/DeepObjectResourceExhaustion.qhelp @@ -0,0 +1,55 @@ + + + + +

+ Processing user-controlled data with a method that allocates excessive amounts + of memory can lead to denial of service. +

+ +

+ If the JSON schema validation library ajv is configured with + allErrors: true 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. +

+
+ + +

+ Do not use allErrors: true in production. +

+
+ + +

+ In the example below, the user-submitted object req.body is + validated using ajv and allErrors: true: +

+ + + +

+ Although this ensures that req.body 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. +

+ +

+ A solution is to not pass in allErrors: true, which means + ajv will only report the first error, not all of them: +

+ + +
+ + +
  • Ajv documentation: security considerations +
  • +
    +
    diff --git a/javascript/ql/src/Security/CWE-400/DeepObjectResourceExhaustion.ql b/javascript/ql/src/Security/CWE-400/DeepObjectResourceExhaustion.ql new file mode 100644 index 00000000000..69ce56fd17f --- /dev/null +++ b/javascript/ql/src/Security/CWE-400/DeepObjectResourceExhaustion.ql @@ -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 diff --git a/javascript/ql/src/Security/CWE-400/examples/DeepObjectResourceExhaustion.js b/javascript/ql/src/Security/CWE-400/examples/DeepObjectResourceExhaustion.js new file mode 100644 index 00000000000..dfeac169558 --- /dev/null +++ b/javascript/ql/src/Security/CWE-400/examples/DeepObjectResourceExhaustion.js @@ -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; + } + // ... +}); diff --git a/javascript/ql/src/Security/CWE-400/examples/DeepObjectResourceExhaustion_fixed.js b/javascript/ql/src/Security/CWE-400/examples/DeepObjectResourceExhaustion_fixed.js new file mode 100644 index 00000000000..0c16bf565b2 --- /dev/null +++ b/javascript/ql/src/Security/CWE-400/examples/DeepObjectResourceExhaustion_fixed.js @@ -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; + } + // ... +}); diff --git a/javascript/ql/src/semmle/javascript/security/dataflow/DeepObjectResourceExhaustion.qll b/javascript/ql/src/semmle/javascript/security/dataflow/DeepObjectResourceExhaustion.qll new file mode 100644 index 00000000000..d4f8b66c699 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/security/dataflow/DeepObjectResourceExhaustion.qll @@ -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) + } + } +} diff --git a/javascript/ql/src/semmle/javascript/security/dataflow/DeepObjectResourceExhaustionCustomizations.qll b/javascript/ql/src/semmle/javascript/security/dataflow/DeepObjectResourceExhaustionCustomizations.qll new file mode 100644 index 00000000000..86d6d19634d --- /dev/null +++ b/javascript/ql/src/semmle/javascript/security/dataflow/DeepObjectResourceExhaustionCustomizations.qll @@ -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" + } + } +}