Merge pull request #4942 from esbena/js/reintroduce-resource-exhaustion

Approved by erik-krogh
This commit is contained in:
CodeQL CI
2021-01-21 01:21:33 -08:00
committed by GitHub
11 changed files with 439 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Applications are constrained by how many resources they can make use
of. Failing to respect these constraints may cause the application to
be unresponsive or crash. It is therefore problematic if attackers
can control the sizes or lifetimes of allocated objects.
</p>
</overview>
<recommendation>
<p>
Ensure that attackers can not control object sizes and their
lifetimes. If object sizes and lifetimes must be controlled by
external parties, ensure you restrict the object sizes and lifetimes so that
they are within acceptable ranges.
</p>
</recommendation>
<example>
<p>
The following example lets a user choose a delay after
which a function is executed:
</p>
<sample src="examples/ResourceExhaustion_timeout.js" />
<p>
This is problematic because a large delay essentially makes the
application wait indefinitely before executing the function. Repeated
registrations of such delays will therefore use up all of the memory
in the application.
A limit on the delay will prevent the attack:
</p>
<sample src="examples/ResourceExhaustion_timeout_fixed.js" />
</example>
<references>
</references>
</qhelp>

View File

@@ -0,0 +1,20 @@
/**
* @name Resource exhaustion
* @description Allocating objects or timers with user-controlled
* sizes or durations can cause resource exhaustion.
* @kind path-problem
* @problem.severity warning
* @id js/resource-exhaustion
* @precision high
* @tags security
* external/cwe/cwe-770
*/
import javascript
import DataFlow::PathGraph
import experimental.semmle.javascript.security.dataflow.ResourceExhaustion::ResourceExhaustion
from Configuration dataflow, DataFlow::PathNode source, DataFlow::PathNode sink
where dataflow.hasFlowPath(source, sink)
select sink, source, sink, sink.getNode().(Sink).getProblemDescription() + " from $@.", source,
"here"

View File

@@ -0,0 +1,9 @@
var http = require("http"),
url = require("url");
var server = http.createServer(function(req, res) {
var delay = parseInt(url.parse(req.url, true).query.delay);
setTimeout(f, delay); // BAD
});

View File

@@ -0,0 +1,15 @@
var http = require("http"),
url = require("url");
var server = http.createServer(function(req, res) {
var delay = parseInt(url.parse(req.url, true).query.delay);
if (delay > 1000) {
res.statusCode = 400;
res.end("Bad request.");
return;
}
setTimeout(f, delay); // GOOD
});

View File

@@ -0,0 +1,70 @@
/**
* Provides a taint tracking configuration for reasoning about
* resource exhaustion vulnerabilities (CWE-770).
*
* Note, for performance reasons: only import this file if
* `ResourceExhaustion::Configuration` is needed, otherwise
* `ResourceExhaustionCustomizations` should be imported instead.
*/
import javascript
import semmle.javascript.security.dataflow.LoopBoundInjectionCustomizations
module ResourceExhaustion {
import ResourceExhaustionCustomizations::ResourceExhaustion
/**
* A data flow configuration for resource exhaustion vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "ResourceExhaustion" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node dst) {
isNumericFlowStep(src, dst)
or
// reuse most existing taint steps
isRestrictedAdditionalTaintStep(src, dst)
}
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
guard instanceof LoopBoundInjection::LengthCheckSanitizerGuard or
guard instanceof UpperBoundsCheckSanitizerGuard
}
}
predicate isRestrictedAdditionalTaintStep(DataFlow::Node src, DataFlow::Node dst) {
any(TaintTracking::AdditionalTaintStep dts).step(src, dst) and
not dst.asExpr() instanceof AddExpr and
not dst.(DataFlow::MethodCallNode).calls(src, "toString")
}
/**
* Holds if data may flow from `src` to `dst` as a number.
*/
predicate isNumericFlowStep(DataFlow::Node src, DataFlow::Node dst) {
// steps that introduce or preserve a number
dst.(DataFlow::PropRead).accesses(src, ["length", "size"])
or
exists(DataFlow::CallNode c |
c = dst and
src = c.getAnArgument()
|
c = DataFlow::globalVarRef("Math").getAMemberCall(_) or
c = DataFlow::globalVarRef(["Number", "parseInt", "parseFloat"]).getACall()
)
or
exists(Expr dstExpr, Expr srcExpr |
dstExpr = dst.asExpr() and
srcExpr = src.asExpr()
|
dstExpr.(BinaryExpr).getAnOperand() = srcExpr and
not dstExpr instanceof AddExpr
or
dstExpr.(PlusExpr).getOperand() = srcExpr
)
}
}

View File

@@ -0,0 +1,80 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* resource exhaustion vulnerabilities, as well as extension points for
* adding your own.
*/
import javascript
module ResourceExhaustion {
/**
* A data flow source for resource exhaustion vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for resource exhaustion vulnerabilities.
*/
abstract class Sink extends DataFlow::Node {
/**
* Gets a description of why this is a problematic sink.
*/
abstract string getProblemDescription();
}
/**
* A data flow sanitizer for resource exhaustion vulnerabilities.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A sanitizer that blocks taint flow if the size of a number is limited.
*/
class UpperBoundsCheckSanitizerGuard extends TaintTracking::SanitizerGuardNode,
DataFlow::ValueNode {
override RelationalComparison astNode;
override predicate sanitizes(boolean outcome, Expr e) {
true = outcome and
e = astNode.getLesserOperand()
or
false = outcome and
e = astNode.getGreaterOperand()
}
}
/** A source of remote user input, considered as a data flow source for resource exhaustion vulnerabilities. */
class RemoteFlowSourceAsSource extends Source {
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
}
/**
* A node that determines the repetitions of a string, considered as a data flow sink for resource exhaustion vulnerabilities.
*/
class StringRepetitionSink extends Sink {
StringRepetitionSink() {
exists(DataFlow::MethodCallNode repeat |
repeat.getMethodName() = "repeat" and
this = repeat.getArgument(0)
)
}
override string getProblemDescription() {
result = "This creates a string with a user-controlled length"
}
}
/**
* A node that determines the duration of a timer, considered as a data flow sink for resource exhaustion vulnerabilities.
*/
class TimerDurationSink extends Sink {
TimerDurationSink() {
this = DataFlow::globalVarRef(["setTimeout", "setInterval"]).getACall().getArgument(1) or
this = LodashUnderscore::member(["delay", "throttle", "debounce"]).getACall().getArgument(1)
}
override string getProblemDescription() {
result = "This creates a timer with a user-controlled duration"
}
}
}

View File

@@ -0,0 +1,68 @@
nodes
| documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay |
| documentaion-examples/ResourceExhaustion_timeout.js:5:14:5:59 | parseIn ... .delay) |
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) |
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:52 | url.par ... ).query |
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:58 | url.par ... y.delay |
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url |
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url |
| documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
| documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
| resource-exhaustion.js:9:7:9:42 | s |
| resource-exhaustion.js:9:11:9:34 | url.par ... , true) |
| resource-exhaustion.js:9:11:9:40 | url.par ... ).query |
| resource-exhaustion.js:9:11:9:42 | url.par ... query.s |
| resource-exhaustion.js:9:21:9:27 | req.url |
| resource-exhaustion.js:9:21:9:27 | req.url |
| resource-exhaustion.js:10:7:10:21 | n |
| resource-exhaustion.js:10:11:10:21 | parseInt(s) |
| resource-exhaustion.js:10:20:10:20 | s |
| resource-exhaustion.js:38:12:38:12 | n |
| resource-exhaustion.js:38:12:38:12 | n |
| resource-exhaustion.js:39:12:39:12 | s |
| resource-exhaustion.js:39:12:39:12 | s |
| resource-exhaustion.js:85:17:85:17 | n |
| resource-exhaustion.js:85:17:85:17 | n |
| resource-exhaustion.js:86:17:86:17 | s |
| resource-exhaustion.js:86:17:86:17 | s |
| resource-exhaustion.js:87:18:87:18 | n |
| resource-exhaustion.js:87:18:87:18 | n |
| resource-exhaustion.js:88:18:88:18 | s |
| resource-exhaustion.js:88:18:88:18 | s |
edges
| documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay | documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
| documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay | documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay |
| documentaion-examples/ResourceExhaustion_timeout.js:5:14:5:59 | parseIn ... .delay) | documentaion-examples/ResourceExhaustion_timeout.js:5:6:5:59 | delay |
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:52 | url.par ... ).query |
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:52 | url.par ... ).query | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:58 | url.par ... y.delay |
| documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:58 | url.par ... y.delay | documentaion-examples/ResourceExhaustion_timeout.js:5:14:5:59 | parseIn ... .delay) |
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) |
| documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | documentaion-examples/ResourceExhaustion_timeout.js:5:23:5:46 | url.par ... , true) |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:10:20:10:20 | s |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:39:12:39:12 | s |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:39:12:39:12 | s |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:86:17:86:17 | s |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:86:17:86:17 | s |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:88:18:88:18 | s |
| resource-exhaustion.js:9:7:9:42 | s | resource-exhaustion.js:88:18:88:18 | s |
| resource-exhaustion.js:9:11:9:34 | url.par ... , true) | resource-exhaustion.js:9:11:9:40 | url.par ... ).query |
| resource-exhaustion.js:9:11:9:40 | url.par ... ).query | resource-exhaustion.js:9:11:9:42 | url.par ... query.s |
| resource-exhaustion.js:9:11:9:42 | url.par ... query.s | resource-exhaustion.js:9:7:9:42 | s |
| resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:9:11:9:34 | url.par ... , true) |
| resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:9:11:9:34 | url.par ... , true) |
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:38:12:38:12 | n |
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:38:12:38:12 | n |
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:85:17:85:17 | n |
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:85:17:85:17 | n |
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:87:18:87:18 | n |
| resource-exhaustion.js:10:7:10:21 | n | resource-exhaustion.js:87:18:87:18 | n |
| resource-exhaustion.js:10:11:10:21 | parseInt(s) | resource-exhaustion.js:10:7:10:21 | n |
| resource-exhaustion.js:10:20:10:20 | s | resource-exhaustion.js:10:11:10:21 | parseInt(s) |
#select
| documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay | documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | documentaion-examples/ResourceExhaustion_timeout.js:7:16:7:20 | delay | This creates a timer with a user-controlled duration from $@. | documentaion-examples/ResourceExhaustion_timeout.js:5:33:5:39 | req.url | here |
| resource-exhaustion.js:38:12:38:12 | n | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:38:12:38:12 | n | This creates a string with a user-controlled length from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
| resource-exhaustion.js:39:12:39:12 | s | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:39:12:39:12 | s | This creates a string with a user-controlled length from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
| resource-exhaustion.js:85:17:85:17 | n | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:85:17:85:17 | n | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
| resource-exhaustion.js:86:17:86:17 | s | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:86:17:86:17 | s | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
| resource-exhaustion.js:87:18:87:18 | n | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:87:18:87:18 | n | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |
| resource-exhaustion.js:88:18:88:18 | s | resource-exhaustion.js:9:21:9:27 | req.url | resource-exhaustion.js:88:18:88:18 | s | This creates a timer with a user-controlled duration from $@. | resource-exhaustion.js:9:21:9:27 | req.url | here |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-770/ResourceExhaustion.ql

View File

@@ -0,0 +1,9 @@
var http = require("http"),
url = require("url");
var server = http.createServer(function(req, res) {
var delay = parseInt(url.parse(req.url, true).query.delay);
setTimeout(f, delay); // BAD
});

View File

@@ -0,0 +1,15 @@
var http = require("http"),
url = require("url");
var server = http.createServer(function(req, res) {
var delay = parseInt(url.parse(req.url, true).query.delay);
if (delay > 1000) {
res.statusCode = 400;
res.end("Bad request.");
return;
}
setTimeout(f, delay); // GOOD
});

View File

@@ -0,0 +1,89 @@
// this file contains many `NOT OK [INCONSISTENCY]` annotations, those
// would be resolved if the query used flow labels to recognice
// numbers flowing to sinks
var http = require("http"),
url = require("url");
var server = http.createServer(function(req, res) {
let s = url.parse(req.url, true).query.s;
let n = parseInt(s);
Buffer.from(s); // OK
Buffer.from(n); // OK
Buffer.from(x, n); // OK
Buffer.from(x, y, s); // NOT OK
Buffer.from(x, y, n); // NOT OK [INCONSISTENCY]
Buffer.from(x, y, n); // NOT OK [INCONSISTENCY]
Buffer.alloc(n); // NOT OK [INCONSISTENCY]
Buffer.allocUnsafe(n); // NOT OK [INCONSISTENCY]
Buffer.allocUnsafeSlow(n); // NOT OK [INCONSISTENCY]
new Buffer(n); // NOT OK [INCONSISTENCY]
new Buffer(x, n); // OK
new Buffer(x, y, n); // NOT OK [INCONSISTENCY]
new SlowBuffer(n); // NOT OK [INCONSISTENCY]
Array(n); // OK
new Array(n); // OK
Array(n).map(); // NOT OK [INCONSISTENCY]
new Array(n).map(); // NOT OK [INCONSISTENCY]
Array(n).fill(); // NOT OK [INCONSISTENCY]
Array(n).join(); // NOT OK [INCONSISTENCY]
Array(n).toString(); // NOT OK [INCONSISTENCY]
Array(n) + x; // NOT OK [INCONSISTENCY]
x.repeat(n); // NOT OK
x.repeat(s); // NOT OK
new Buffer(n * x); // NOT OK [INCONSISTENCY]
new Buffer(n + n); // NOT OK [INCONSISTENCY]
new Buffer(n + x); // OK (maybe)
new Buffer(n + s); // OK (this is a string if `s` is a string)
new Buffer(s + 2); // OK (this is a string if `s` is a string)
new Buffer(s + s); // OK
new Buffer(n + "X"); // OK
new Buffer(Math.ceil(s)); // NOT OK [INCONSISTENCY]
new Buffer(Number(s)); // NOT OK [INCONSISTENCY]
new Buffer(new Number(s)); // OK
new Buffer(s + x.length); // OK (this is a string if `s` is a string)
new Buffer(s.length); // NOT OK [INCONSISTENCY]
if (n < 100) {
new Buffer(n); // OK
} else {
new Buffer(n); // NOT OK [INCONSISTENCY]
}
let ns = x ? n : s;
new Buffer(ns); // NOT OK [INCONSISTENCY]
new Buffer(n.toString()); // OK
if (typeof n === "string") {
new Buffer(n); // OK
} else {
new Buffer(n); // NOT OK [INCONSISTENCY]
}
if (typeof n === "number") {
new Buffer(n); // NOT OK [INCONSISTENCY]
} else {
new Buffer(n); // OK
}
if (typeof s === "number") {
new Buffer(s); // NOT OK [INCONSISTENCY]
} else {
new Buffer(s); // OK
}
setTimeout(f, n); // NOT OK
setTimeout(f, s); // NOT OK
setInterval(f, n); // NOT OK
setInterval(f, s); // NOT OK
});