Skip to content

Commit 24199a5

Browse files
committed
JS: Add query for resource exhaustion from deep object handling
1 parent b978359 commit 24199a5

File tree

6 files changed

+227
-0
lines changed

6 files changed

+227
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Processing user-controlled data with a method that allocates excessive amounts
9+
of memory can lead to denial of service.
10+
</p>
11+
12+
<p>
13+
If the JSON schema validation library <code>ajv</code> is configured with
14+
<code>allErrors: true</code> there is no limit to how many error objects
15+
will be allocated. An attacker can exploit this by sending an object that
16+
deliberately contains a huge number of errors, and in some cases, with
17+
longer and longer error messages. This can cause the service to become
18+
unresponsive due to the slow error-checking process.
19+
</p>
20+
</overview>
21+
22+
<recommendation>
23+
<p>
24+
Do not use <code>allErrors: true</code> in production.
25+
</p>
26+
</recommendation>
27+
28+
<example>
29+
<p>
30+
In the example below, the user-submitted object <code>req.body</code> is
31+
validated using <code>ajv</code> and <code>allErrors: true</code>:
32+
</p>
33+
34+
<sample src="examples/DeepObjectResourceExhaustion.js"/>
35+
36+
<p>
37+
Although this ensures that <code>req.body</code> conforms to the schema,
38+
the validation itself could be vulnerable to a denial-of-service attack.
39+
An attacker could send an object containing so many errors that the server
40+
runs out of memory.
41+
</p>
42+
43+
<p>
44+
A solution is to not pass in <code>allErrors: true</code>, which means
45+
<code>ajv</code> will only report the first error, not all of them:
46+
</p>
47+
48+
<sample src="examples/DeepObjectResourceExhaustion_fixed.js"/>
49+
</example>
50+
51+
<references>
52+
<li>Ajv documentation: <a href="https://github.com/ajv-validator/ajv/blob/master/docs/security.md#untrusted-schemas">security considerations</a>
53+
</li>
54+
</references>
55+
</qhelp>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @name Resources exhaustion from deep object traversal
3+
* @description Processing user-controlled object hierarchies inefficiently can lead to denial of service.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @precision high
7+
* @id js/resource-exhaustion-from-deep-object-traversal
8+
* @tags security
9+
* external/cwe/cwe-400
10+
*/
11+
12+
import javascript
13+
import DataFlow::PathGraph
14+
import semmle.javascript.security.dataflow.DeepObjectResourceExhaustion::DeepObjectResourceExhaustion
15+
16+
from
17+
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node link,
18+
string reason
19+
where
20+
cfg.hasFlowPath(source, sink) and
21+
sink.getNode().(Sink).hasReason(link, reason)
22+
select sink, source, sink, "Denial of service caused by processing user input from $@ with $@.",
23+
source.getNode(), "here", link, reason
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import express from 'express';
2+
import Ajv from 'ajv';
3+
4+
let ajv = new Ajv({ allErrors: true });
5+
ajv.addSchema(require('./input-schema'), 'input');
6+
7+
var app = express();
8+
app.get('/user/:id', function(req, res) {
9+
if (!ajv.validate('input', req.body)) {
10+
res.end(ajv.errorsText());
11+
return;
12+
}
13+
// ...
14+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import express from 'express';
2+
import Ajv from 'ajv';
3+
4+
let ajv = new Ajv({ allErrors: process.env['REST_DEBUG'] });
5+
ajv.addSchema(require('./input-schema'), 'input');
6+
7+
var app = express();
8+
app.get('/user/:id', function(req, res) {
9+
if (!ajv.validate('input', req.body)) {
10+
res.end(ajv.errorsText());
11+
return;
12+
}
13+
// ...
14+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about DoS attacks
3+
* due to inefficient handling of user-controlled objects.
4+
*/
5+
6+
import javascript
7+
import semmle.javascript.security.TaintedObject
8+
9+
module DeepObjectResourceExhaustion {
10+
import DeepObjectResourceExhaustionCustomizations::DeepObjectResourceExhaustion
11+
12+
/**
13+
* A taint tracking configuration for reasoning about DoS attacks due to inefficient handling
14+
* of user-controlled objects.
15+
*/
16+
class Configuration extends TaintTracking::Configuration {
17+
Configuration() { this = "DeepObjectResourceExhaustion" }
18+
19+
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
20+
source instanceof Source and label = TaintedObject::label()
21+
or
22+
// We currently can't expose the TaintedObject label in the Customizations library
23+
// so just add its default sources here.
24+
source instanceof TaintedObject::Source and label = TaintedObject::label()
25+
or
26+
source instanceof RemoteFlowSource and label.isTaint()
27+
}
28+
29+
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
30+
sink instanceof Sink and label = TaintedObject::label()
31+
}
32+
33+
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
34+
guard instanceof TaintedObject::SanitizerGuard
35+
}
36+
37+
override predicate isAdditionalFlowStep(
38+
DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl
39+
) {
40+
TaintedObject::step(src, trg, inlbl, outlbl)
41+
}
42+
}
43+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Provides sources, sinks and sanitizers for reasoning about
3+
* DoS attacks due to inefficient handling of user-controlled objects.
4+
*/
5+
6+
import javascript
7+
8+
/**
9+
* Provides sources, sinks and sanitizers for reasoning about
10+
* DoS attacks due to inefficient handling of user-controlled objects.
11+
*/
12+
module DeepObjectResourceExhaustion {
13+
/**
14+
* A data flow source for slow input validation.
15+
*/
16+
abstract class Source extends DataFlow::Node { }
17+
18+
/**
19+
* A data flow sink for slow input validation.
20+
*/
21+
abstract class Sink extends DataFlow::Node {
22+
/**
23+
* Holds if `link` and `text` should be included in the message to explain
24+
* why the input validation is slow.
25+
*/
26+
abstract predicate hasReason(DataFlow::Node link, string text);
27+
}
28+
29+
/**
30+
* A sanitizer for slow input validation.
31+
*/
32+
abstract class Sanitizer extends DataFlow::Node { }
33+
34+
/** Gets a node that may refer to an object with `allErrors` set to `true`. */
35+
private DataFlow::SourceNode allErrorsObject(
36+
DataFlow::TypeTracker t, DataFlow::PropWrite allErrors
37+
) {
38+
t.start() and
39+
exists(JsonSchema::Ajv::AjvValidationCall call) and // only compute if `ajv` is used
40+
allErrors.getPropertyName() = "allErrors" and
41+
allErrors.getRhs().mayHaveBooleanValue(true) and
42+
result = allErrors.getBase().getALocalSource()
43+
or
44+
exists(ExtendCall call |
45+
allErrorsObject(t.continue(), allErrors).flowsTo(call.getAnOperand()) and
46+
(result = call or result = call.getDestinationOperand().getALocalSource())
47+
)
48+
or
49+
exists(DataFlow::ObjectLiteralNode obj |
50+
allErrorsObject(t.continue(), allErrors).flowsTo(obj.getASpreadProperty()) and
51+
result = obj
52+
)
53+
or
54+
exists(DataFlow::TypeTracker t2 | result = allErrorsObject(t2, allErrors).track(t2, t))
55+
}
56+
57+
/** Gets a node that may refer to an object with `allErrors` set to `true`. */
58+
private DataFlow::SourceNode allErrorsObject(DataFlow::PropWrite allErrors) {
59+
result = allErrorsObject(DataFlow::TypeTracker::end(), allErrors)
60+
}
61+
62+
/** Argument to an `ajv` validation call configured with `allErrors: true`. */
63+
private class AjvValidationSink extends Sink {
64+
DataFlow::PropWrite allErrors;
65+
66+
AjvValidationSink() {
67+
exists(JsonSchema::Ajv::AjvValidationCall call |
68+
this = call.getInput() and
69+
allErrorsObject(allErrors).flowsTo(call.getAnOptionsArg())
70+
)
71+
}
72+
73+
override predicate hasReason(DataFlow::Node link, string text) {
74+
link = allErrors and
75+
text = "allErrors: true"
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)