Skip to content

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
35 changes: 35 additions & 0 deletions java/ql/src/Security/CWE/CWE-789/UnboundedAllocation.qhelp
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>

Copy link
Contributor

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?

</references>
</qhelp>
29 changes: 29 additions & 0 deletions java/ql/src/Security/CWE/CWE-789/UnboundedAllocation.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @name Unbounded allocation
* @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];

// ...
}
72 changes: 72 additions & 0 deletions java/ql/src/Security/CWE/CWE-789/UnboundedAllocationCommon.qll
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() |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there exists already an ArithExpr.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also cover readUnshared()?

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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the class name match the file name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
}
}
}
14 changes: 14 additions & 0 deletions java/ql/src/Security/CWE/CWE-789/UnboundedAllocationGood.java
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];

// ...
}
81 changes: 81 additions & 0 deletions java/ql/test/query-tests/security/CWE-789/Test.java
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
Loading
Oops, something went wrong.