Skip to content

Commit ff5b873

Browse files
authored
Merge pull request github#8773 from erik-krogh/exhaustion
JS: promote `js/resource-exhaustion` out of experimental
2 parents 8bd975a + 8669bbd commit ff5b873

30 files changed

+627
-388
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* resource exhaustion vulnerabilities, as well as extension points for
4+
* adding your own.
5+
*/
6+
7+
import javascript
8+
9+
/**
10+
* Provides sources, sinks, and sanitizers for reasoning about
11+
* resource exhaustion vulnerabilities.
12+
*/
13+
module ResourceExhaustion {
14+
/**
15+
* A data flow source for resource exhaustion vulnerabilities.
16+
*/
17+
abstract class Source extends DataFlow::Node { }
18+
19+
/**
20+
* A data flow sink for resource exhaustion vulnerabilities.
21+
*/
22+
abstract class Sink extends DataFlow::Node {
23+
/**
24+
* Gets a description of why this is a problematic sink.
25+
*/
26+
abstract string getProblemDescription();
27+
}
28+
29+
/**
30+
* A data flow sanitizer for resource exhaustion vulnerabilities.
31+
*/
32+
abstract class Sanitizer extends DataFlow::Node { }
33+
34+
/** A source of remote user input, considered as a data flow source for resource exhaustion vulnerabilities. */
35+
class RemoteFlowSourceAsSource extends Source instanceof RemoteFlowSource {
36+
RemoteFlowSourceAsSource() {
37+
// exclude source that only happen client-side
38+
not this instanceof ClientSideRemoteFlowSource and
39+
not this = DataFlow::parameterNode(any(PostMessageEventHandler pmeh).getEventParameter())
40+
}
41+
}
42+
43+
/**
44+
* A node that determines the repetitions of a string, considered as a data flow sink for resource exhaustion vulnerabilities.
45+
*/
46+
class StringRepetitionSink extends Sink {
47+
StringRepetitionSink() {
48+
exists(DataFlow::MethodCallNode repeat |
49+
repeat.getMethodName() = "repeat" and
50+
this = repeat.getArgument(0)
51+
)
52+
}
53+
54+
override string getProblemDescription() {
55+
result = "This creates a string with a user-controlled length"
56+
}
57+
}
58+
59+
/**
60+
* A node that determines the duration of a timer, considered as a data flow sink for resource exhaustion vulnerabilities.
61+
*/
62+
class TimerDurationSink extends Sink {
63+
TimerDurationSink() {
64+
this = DataFlow::globalVarRef(["setTimeout", "setInterval"]).getACall().getArgument(1) or
65+
this = LodashUnderscore::member(["delay", "throttle", "debounce"]).getACall().getArgument(1)
66+
}
67+
68+
override string getProblemDescription() {
69+
result = "This creates a timer with a user-controlled duration"
70+
}
71+
}
72+
73+
/**
74+
* A node that determines the size of a buffer, considered as a data flow sink for resource exhaustion vulnerabilities.
75+
*/
76+
class BufferSizeSink extends Sink {
77+
BufferSizeSink() {
78+
exists(DataFlow::SourceNode clazz, DataFlow::InvokeNode invk, int index |
79+
clazz = DataFlow::globalVarRef("Buffer") and this = invk.getArgument(index)
80+
|
81+
exists(string name |
82+
invk = clazz.getAMemberCall(name) and
83+
(
84+
name = "from" and index = 2 // the length argument
85+
or
86+
name = ["alloc", "allocUnsafe", "allocUnsafeSlow"] and index = 0 // the buffer size
87+
)
88+
)
89+
or
90+
invk = clazz.getAnInvocation() and
91+
(
92+
// invk.getNumArgument() = 1 and // `new Buffer(size)`, it's only an issue if the size is a number, which we don't track precisely.
93+
// index = 0
94+
// or
95+
invk.getNumArgument() = 3 and index = 2 // the length argument
96+
)
97+
)
98+
or
99+
this = DataFlow::globalVarRef("SlowBuffer").getAnInstantiation().getArgument(0)
100+
}
101+
102+
override string getProblemDescription() {
103+
result = "This creates a buffer with a user-controlled size"
104+
}
105+
}
106+
107+
/**
108+
* A node that determines the size of an array, considered as a data flow sink for resource exhaustion vulnerabilities.
109+
* This is only an issue if the argument is a number, which we don't track precisely.
110+
*/
111+
class DenseArraySizeSink extends Sink {
112+
DenseArraySizeSink() {
113+
// Arrays are sparse by default, so we must also look at how the array is used
114+
exists(DataFlow::ArrayConstructorInvokeNode instance |
115+
this = instance.getArgument(0) and
116+
instance.getNumArgument() = 1
117+
|
118+
exists(instance.getAMethodCall(["map", "fill", "join", "toString"])) or
119+
instance.flowsToExpr(any(AddExpr p).getAnOperand())
120+
)
121+
}
122+
123+
override string getProblemDescription() {
124+
result = "This creates an array with a user-controlled length"
125+
}
126+
}
127+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about
3+
* resource exhaustion vulnerabilities (CWE-770).
4+
*
5+
* Note, for performance reasons: only import this file if
6+
* `ResourceExhaustion::Configuration` is needed, otherwise
7+
* `ResourceExhaustionCustomizations` should be imported instead.
8+
*/
9+
10+
import javascript
11+
import ResourceExhaustionCustomizations::ResourceExhaustion
12+
13+
/**
14+
* A data flow configuration for resource exhaustion vulnerabilities.
15+
*/
16+
class Configuration extends TaintTracking::Configuration {
17+
Configuration() { this = "ResourceExhaustion" }
18+
19+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
20+
21+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
22+
23+
override predicate isSanitizer(DataFlow::Node node) {
24+
super.isSanitizer(node) or
25+
node instanceof Sanitizer
26+
}
27+
28+
override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node dst) {
29+
isNumericFlowStep(src, dst)
30+
}
31+
32+
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
33+
guard instanceof UpperBoundsCheckSanitizerGuard
34+
}
35+
36+
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
37+
succ.(DataFlow::PropRead).accesses(pred, "length")
38+
}
39+
}
40+
41+
/** Holds if data is converted to a number from `src` to `dst`. */
42+
predicate isNumericFlowStep(DataFlow::Node src, DataFlow::Node dst) {
43+
exists(DataFlow::CallNode c |
44+
c = dst and
45+
src = c.getAnArgument()
46+
|
47+
c = DataFlow::globalVarRef("Math").getAMemberCall(_) or
48+
c = DataFlow::globalVarRef(["Number", "parseInt", "parseFloat"]).getACall()
49+
)
50+
}
51+
52+
/**
53+
* A sanitizer that blocks taint flow if the size of a number is limited.
54+
*/
55+
class UpperBoundsCheckSanitizerGuard extends TaintTracking::SanitizerGuardNode, DataFlow::ValueNode {
56+
override RelationalComparison astNode;
57+
58+
override predicate sanitizes(boolean outcome, Expr e) {
59+
true = outcome and
60+
e = astNode.getLesserOperand()
61+
or
62+
false = outcome and
63+
e = astNode.getGreaterOperand()
64+
}
65+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
8+
<p>
9+
10+
Applications are constrained by how many resources they can make use
11+
of. Failing to respect these constraints may cause the application to
12+
be unresponsive or crash. It is therefore problematic if attackers
13+
can control the sizes or lifetimes of allocated objects.
14+
15+
</p>
16+
17+
</overview>
18+
19+
<recommendation>
20+
21+
<p>
22+
23+
Ensure that attackers can not control object sizes and their
24+
lifetimes. If object sizes and lifetimes must be controlled by
25+
external parties, ensure you restrict the object sizes and lifetimes so that
26+
they are within acceptable ranges.
27+
28+
</p>
29+
30+
</recommendation>
31+
32+
<example>
33+
34+
<p>
35+
36+
The following example allocates a buffer with a user-controlled
37+
size.
38+
39+
</p>
40+
41+
<sample src="examples/ResourceExhaustion_buffer.js" />
42+
43+
<p>
44+
45+
This is problematic since an attacker can choose a size
46+
that makes the application run out of memory. Even worse, in older
47+
versions of Node.js, this could leak confidential memory.
48+
49+
To prevent such attacks, limit the buffer size:
50+
51+
</p>
52+
53+
<sample src="examples/ResourceExhaustion_buffer_fixed.js" />
54+
55+
</example>
56+
57+
<example>
58+
59+
<p>
60+
61+
As another example, consider an application that allocates an
62+
array with a user-controlled size, and then fills it with values:
63+
64+
</p>
65+
66+
<sample src="examples/ResourceExhaustion_array.js" />
67+
68+
<p>
69+
The allocation of the array itself is not problematic since arrays are
70+
allocated sparsely, but the subsequent filling of the array will take
71+
a long time, causing the application to be unresponsive, or even run
72+
out of memory.
73+
74+
Again, a limit on the size will prevent the attack:
75+
76+
</p>
77+
78+
<sample src="examples/ResourceExhaustion_array_fixed.js" />
79+
80+
</example>
81+
82+
<example>
83+
84+
<p>
85+
86+
Finally, the following example lets a user choose a delay after
87+
which a function is executed:
88+
89+
</p>
90+
91+
<sample src="examples/ResourceExhaustion_timeout.js" />
92+
93+
<p>
94+
95+
This is problematic because a large delay essentially makes the
96+
application wait indefinitely before executing the function. Repeated
97+
registrations of such delays will therefore use up all of the memory
98+
in the application.
99+
100+
A limit on the delay will prevent the attack:
101+
102+
</p>
103+
104+
<sample src="examples/ResourceExhaustion_timeout_fixed.js" />
105+
106+
107+
</example>
108+
109+
<references>
110+
<li>
111+
Wikipedia: <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">Denial-of-service attack</a>.
112+
</li>
113+
</references>
114+
115+
</qhelp>

javascript/ql/src/experimental/Security/CWE-770/ResourceExhaustion.ql renamed to javascript/ql/src/Security/CWE-770/ResourceExhaustion.ql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
* sizes or durations can cause resource exhaustion.
55
* @kind path-problem
66
* @problem.severity warning
7+
* @security-severity 7.5
78
* @id js/resource-exhaustion
89
* @precision high
910
* @tags security
11+
* external/cwe/cwe-400
1012
* external/cwe/cwe-770
1113
*/
1214

1315
import javascript
1416
import DataFlow::PathGraph
15-
import experimental.semmle.javascript.security.dataflow.ResourceExhaustion::ResourceExhaustion
17+
import semmle.javascript.security.dataflow.ResourceExhaustionQuery
1618

1719
from Configuration dataflow, DataFlow::PathNode source, DataFlow::PathNode sink
1820
where dataflow.hasFlowPath(source, sink)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var http = require("http"),
2+
url = require("url");
3+
4+
var server = http.createServer(function(req, res) {
5+
var size = parseInt(url.parse(req.url, true).query.size);
6+
7+
let dogs = new Array(size).fill("dog"); // BAD
8+
9+
// ... use the dog
10+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
var http = require("http"),
2+
url = require("url");
3+
4+
var server = http.createServer(function(req, res) {
5+
var size = parseInt(url.parse(req.url, true).query.size);
6+
7+
if (size > 1024) {
8+
res.statusCode = 400;
9+
res.end("Bad request.");
10+
return;
11+
}
12+
13+
let dogs = new Array(size).fill("dog"); // GOOD
14+
15+
// ... use the dogs
16+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var http = require("http"),
2+
url = require("url");
3+
4+
var server = http.createServer(function(req, res) {
5+
var size = parseInt(url.parse(req.url, true).query.size);
6+
7+
let buffer = Buffer.alloc(size); // BAD
8+
9+
// ... use the buffer
10+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
var http = require("http"),
2+
url = require("url");
3+
4+
var server = http.createServer(function(req, res) {
5+
var size = parseInt(url.parse(req.url, true).query.size);
6+
7+
if (size > 1024) {
8+
res.statusCode = 400;
9+
res.end("Bad request.");
10+
return;
11+
}
12+
13+
let buffer = Buffer.alloc(size); // GOOD
14+
15+
// ... use the buffer
16+
});

0 commit comments

Comments
 (0)