-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Java: Unbounded Allocation queries #4582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0902807
9495c5e
e6260b3
94136a7
1e7593e
751c6c6
9096e20
62cd902
79d4b01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
class A implements Serializable{ | ||
transient Object[] data; | ||
|
||
private void writeObject(java.io.ObjectOutputStream out) throws IOException { | ||
out.writeInt(data.length); | ||
|
||
for (int i = 0; i < data.length; i++) { | ||
out.writeObject(data[i]); | ||
} | ||
} | ||
|
||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { | ||
int length = in.readInt(); | ||
|
||
// GOOD: An ArrayList is used, which dynamically allocates memory as needed. | ||
ArrayList<Object> dataList = new ArrayList<>(); | ||
|
||
for (int i = 0; i < length; i++) { | ||
dataList.add(in.readObject()); | ||
} | ||
|
||
data = dataList.toArray(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
<p> | ||
When user input is used to control how much memory is allocated, such as in the size of an array, care should be taken to validate this number | ||
and ensure it is within a sensible bound. | ||
Otherwise, a malicous user may be able to cause a DoS (Denial of Service) attack by causing the application to consume a large amount of memory. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p>Ensure that user-provided sizes are within some upper bound. | ||
Alternatively, a structure such as an <code>ArrayList</code> can be used to ensure that the application only allocates as much memory as is necessary.</p> | ||
|
||
</recommendation> | ||
<example> | ||
|
||
<p> | ||
The following example allocates an amount of memory directly from a user-provided value: | ||
</p> | ||
|
||
<sample src="UnboundedAllocationBad.java" /> | ||
|
||
<p>The following example first validates the user input against an upper bound:</p> | ||
|
||
<sample src="UnboundedAllocationGood.java" /> | ||
|
||
</example> | ||
<references> | ||
|
||
</references> | ||
</qhelp> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/** | ||
* @name Unbounded allocation | ||
joefarebrother marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @description Allocating an unbounded amount of memory from a user-provided value can lead to a Denial of Service attack. | ||
* @kind path-problem | ||
* @problem.severity warning | ||
* @precision medium | ||
* @id java/unbounded-allocation | ||
* @tags security | ||
* external/cwe/cwe-789 | ||
*/ | ||
|
||
import java | ||
import UnboundedAllocationCommon | ||
import semmle.code.java.dataflow.FlowSources | ||
import DataFlow::PathGraph | ||
|
||
class UnboundedAllocationConfig extends TaintTracking::Configuration { | ||
UnboundedAllocationConfig() { this = "UnboundedAllocationConfig" } | ||
|
||
override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } | ||
|
||
override predicate isSink(DataFlow::Node sink) { sink instanceof AllocationSink } | ||
|
||
override predicate isSanitizer(DataFlow::Node node) { hasUpperBound(node.asExpr()) } | ||
} | ||
|
||
from DataFlow::PathNode source, DataFlow::PathNode sink, UnboundedAllocationConfig config | ||
where config.hasFlowPath(source, sink) | ||
select sink, source, sink, "Unbounded memory allocation from $@.", source, "this user input" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
public void processData(Socket sock) { | ||
ObjectInputStream stream = new ObjectInputStream(sock.getInputStream()); | ||
|
||
int size = stream.readInt(); | ||
// BAD: A user-controlled amount of memory is allocated | ||
byte[] buffer = new byte[size]; | ||
|
||
// ... | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* Common definitions for the unbounded allocation queries. | ||
*/ | ||
|
||
import semmle.code.java.dataflow.RangeAnalysis | ||
import semmle.code.java.dataflow.DataFlow | ||
import semmle.code.java.dataflow.TaintTracking | ||
|
||
/** A sink where memory is allocated. */ | ||
class AllocationSink extends DataFlow::Node { | ||
AllocationSink() { | ||
this.asExpr() = any(ArrayCreationExpr a).getADimension() | ||
or | ||
exists(Call c, int i | | ||
c.getArgument(i) = this.asExpr() and | ||
c.getCallee().(AllocatingCallable).getParam() = i | ||
) | ||
} | ||
} | ||
|
||
/** A callable that allocates memory. */ | ||
abstract class AllocatingCallable extends Callable { | ||
/** Returns the parameter index controlling the size of the allocated memory. */ | ||
abstract int getParam(); | ||
} | ||
|
||
private class AtomicArrayConstructor extends AllocatingCallable, Constructor { | ||
AtomicArrayConstructor() { | ||
this | ||
.getDeclaringType() | ||
.hasQualifiedName("java.util.concurrent.atomic", | ||
["AtomicIntegerArray", "AtomicLongArray", "AtomicReferenceArray"]) and | ||
this.getParameterType(0) instanceof IntegralType | ||
} | ||
|
||
override int getParam() { result = 0 } | ||
} | ||
|
||
private class ListConstructor extends AllocatingCallable, Constructor { | ||
ListConstructor() { | ||
this.getDeclaringType().hasQualifiedName("java.util", ["ArrayList", "Vector"]) and | ||
this.getParameterType(0) instanceof IntegralType | ||
} | ||
|
||
override int getParam() { result = 0 } | ||
} | ||
|
||
private class ArithmeticStep extends TaintTracking::AdditionalTaintStep { | ||
override predicate step(DataFlow::Node src, DataFlow::Node sink) { | ||
exists(BinaryExpr binex | sink.asExpr() = binex and src.asExpr() = binex.getAnOperand() | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that there exists already an |
||
binex instanceof AddExpr | ||
or | ||
binex instanceof MulExpr | ||
or | ||
binex instanceof SubExpr | ||
or | ||
binex instanceof LShiftExpr | ||
or | ||
src.asExpr() = binex.getLeftOperand() and | ||
( | ||
binex instanceof DivExpr | ||
or | ||
binex instanceof RShiftExpr | ||
or | ||
binex instanceof URShiftExpr | ||
) | ||
) | ||
} | ||
} | ||
|
||
/** Holds if `e` has a known upper bound. */ | ||
predicate hasUpperBound(Expr e) { bounded(e, any(ZeroBound z), _, true, _) } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
<p> | ||
A custom deserialization method may be expected to be run on an untrusted data stream. | ||
If so, care should be taken when allocating resources such as memory based on a number within that stream. | ||
Otherwise, an attacker may be able to cause a DoS (Denial of Service) attack by providing the application with a maliciously crafted | ||
data stream causing it to consume an unbounded amount of memory. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p> | ||
If a deserialization method for a class is intended to be used on untrusted data, avoid allocating memory up front during deserialization, | ||
instead consider using a structure such as <code>ArrayList</code> that grows proportionally to the size of the input stream. | ||
Alternatively, a maximum size may be enforced. | ||
</p> | ||
|
||
</recommendation> | ||
<example> | ||
|
||
<p> | ||
The following example allocates an array of a size provided by the input stream, making it vulnerable to a DoS attack: | ||
</p> | ||
|
||
<sample src="UnboundedAllocationDeserializationBad.java" /> | ||
|
||
<p> | ||
The following example uses an <code>ArrayList</code> to only allocate as much memory as needed, within a constant factor: | ||
</p> | ||
|
||
<sample src="UnboundedAllocationDeserializationGood.java" /> | ||
</example> | ||
<references> | ||
|
||
<li>Example in the Guava framework: <a href="https://github.com/google/guava/wiki/CVE-2018-10237">CVE-2018-10237</a>, patched by <a href="https://github.com/google/guava/commit/f89ece5721b2f637fe754937ff1f3c86d80bb196">this commit</a>.</li> | ||
</references> | ||
</qhelp> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* @name Unbounded allocation during deserialization | ||
* @description Allocating an unbounded amount of memory during a deserialization method can lead to a Denial of Service attack. | ||
* @kind path-problem | ||
* @problem.severity warning | ||
* @precision medium | ||
* @id java/unbounded-allocation-deserialization | ||
* @tags security | ||
* external/cwe/cwe-789 | ||
*/ | ||
|
||
import java | ||
import UnboundedAllocationCommon | ||
import DataFlow::PathGraph | ||
|
||
class DeserializeMethod extends Method { | ||
DeserializeMethod() { | ||
this.getDeclaringType().getASourceSupertype*() instanceof TypeSerializable and | ||
this.hasName("readObject") and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe also cover |
||
this.getParameterType(0).(RefType).hasQualifiedName("java.io", "ObjectInputStream") | ||
} | ||
|
||
Parameter getInputStreamParameter() { result = getParameter(0) } | ||
} | ||
|
||
class DeserializeParameter extends DataFlow::Node { | ||
DeserializeParameter() { this.asParameter() = any(DeserializeMethod m).getInputStreamParameter() } | ||
} | ||
|
||
class UnboundedDeserializationConfig extends TaintTracking::Configuration { | ||
UnboundedDeserializationConfig() { this = "UnboundedDeserializationConfig" } | ||
|
||
override predicate isSource(DataFlow::Node src) { src instanceof DeserializeParameter } | ||
|
||
override predicate isSink(DataFlow::Node sink) { sink instanceof AllocationSink } | ||
|
||
override predicate isSanitizer(DataFlow::Node node) { hasUpperBound(node.asExpr()) } | ||
} | ||
|
||
from DataFlow::PathNode source, DataFlow::PathNode sink, UnboundedDeserializationConfig config | ||
where config.hasFlowPath(source, sink) | ||
select sink, source, sink, "Unbounded memory allocation during deserialization of $@.", source, | ||
"this input stream" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
class A implements Serializable{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the class name match the file name? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is intended to be included as a snipet in the qhelp file rather than compiled on its own, so it shouldn't matter that the class name doesn't match the file name. |
||
transient Object[] data; | ||
|
||
private void writeObject(java.io.ObjectOutputStream out) throws IOException { | ||
out.writeInt(data.length); | ||
|
||
for (int i = 0; i < data.length; i++) { | ||
out.writeObject(data[i]); | ||
} | ||
} | ||
|
||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { | ||
int length = in.readInt(); | ||
|
||
// BAD: An array is allocated whose size could be controlled by an attacker. | ||
data = new Object[length]; | ||
|
||
for (int i = 0; i < length; i++) { | ||
data[i] = in.readObject(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
public void processData(Socket sock) { | ||
ObjectInputStream stream = new ObjectInputStream(sock.getInputStream()); | ||
|
||
int size = stream.readInt(); | ||
|
||
if (size > 4096) { | ||
throw new IOException("Size too large"); | ||
} | ||
|
||
// GOOD: There is an upper bound on memory consumption. | ||
byte[] buffer = new byte[size]; | ||
|
||
// ... | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import java.io.*; | ||
import java.lang.ClassNotFoundException; | ||
import java.net.Socket; | ||
import java.util.ArrayList; | ||
|
||
class Test { | ||
|
||
class A implements Serializable{ | ||
transient Object[] data; | ||
|
||
private void writeObject(java.io.ObjectOutputStream out) throws IOException { | ||
out.writeInt(data.length); | ||
|
||
for (int i = 0; i < data.length; i++) { | ||
out.writeObject(data[i]); | ||
} | ||
} | ||
|
||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { | ||
int length = in.readInt(); | ||
|
||
// BAD: An array is allocated whose size could be controlled by an attacker. | ||
data = new Object[length]; | ||
|
||
for (int i = 0; i < length; i++) { | ||
data[i] = in.readObject(); | ||
} | ||
} | ||
} | ||
|
||
class B implements Serializable{ | ||
transient Object[] data; | ||
|
||
private void writeObject(java.io.ObjectOutputStream out) throws IOException { | ||
out.writeInt(data.length); | ||
|
||
for (int i = 0; i < data.length; i++) { | ||
out.writeObject(data[i]); | ||
} | ||
} | ||
|
||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { | ||
int length = in.readInt(); | ||
|
||
// GOOD: An ArrayList is used, which dynamically allocates memory as needed. | ||
ArrayList<Object> dataList = new ArrayList<>(); | ||
|
||
for (int i = 0; i < length; i++) { | ||
dataList.add(in.readObject()); | ||
} | ||
|
||
data = dataList.toArray(); | ||
} | ||
} | ||
|
||
|
||
public void processData1(Socket sock) throws IOException { | ||
ObjectInputStream stream = new ObjectInputStream(sock.getInputStream()); | ||
|
||
int size = stream.readInt(); | ||
// BAD: A user-controlled amount of memory is allocated | ||
byte[] buffer = new byte[size]; | ||
|
||
// ... | ||
} | ||
|
||
public void processData2(Socket sock) throws IOException { | ||
ObjectInputStream stream = new ObjectInputStream(sock.getInputStream()); | ||
|
||
int size = stream.readInt(); | ||
|
||
if (size > 4096) { | ||
throw new IOException("Size too large"); | ||
} | ||
|
||
// GOOD: There is an upper bound on memory consumption. | ||
byte[] buffer = new byte[size]; | ||
|
||
// ... | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
edges | ||
| Test.java:58:58:58:78 | getInputStream(...) : InputStream | Test.java:62:34:62:37 | size | | ||
nodes | ||
| Test.java:58:58:58:78 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | | ||
| Test.java:62:34:62:37 | size | semmle.label | size | | ||
#select | ||
| Test.java:62:34:62:37 | size | Test.java:58:58:58:78 | getInputStream(...) : InputStream | Test.java:62:34:62:37 | size | Unbounded memory allocation from $@. | Test.java:58:58:58:78 | getInputStream(...) : InputStream | this user input | |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Security/CWE/CWE-789/UnboundedAllocation.ql |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any references that it would make sense to add here alongside the automated CWE reference? Possibly it's worth mentioning the CVE that inspired this?