Skip to content
Open
Show file tree
Hide file tree
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
13 changes: 13 additions & 0 deletions java/org/brotli/wrapper/dec/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,16 @@ java_test(
test_class = "org.brotli.wrapper.dec.DecoderJNITest",
runtime_deps = [":test_lib"],
)

java_test(
name = "DecompressLimitTest",
size = "large",
data = [
":brotli_jni", # Bazel JNI workaround
],
jvm_flags = [
"-DBROTLI_JNI_LIBRARY=$(location :brotli_jni)",
],
test_class = "org.brotli.wrapper.dec.DecompressLimitTest",
runtime_deps = [":test_lib"],
)
44 changes: 43 additions & 1 deletion java/org/brotli/wrapper/dec/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ public class Decoder implements AutoCloseable {
boolean closed;
boolean eager;

/** Decoder configuration. */
public static final class Parameters {
private int maxOutputSize;

public Parameters() { }

/**
* Limits the total decompressed output produced by {@link #decompress}.
*
* <p>Use this to mitigate decompression bombs when decoding untrusted input:
* {@code decompress} throws {@link IOException} as soon as the decoded size
* would exceed {@code maxOutputSize}.
*
* @param maxOutputSize maximum total decompressed bytes; {@code 0} (the
* default) or negative means no limit
*/
public Parameters setMaxOutputSize(int maxOutputSize) {
this.maxOutputSize = maxOutputSize;
return this;
}
}

/**
* Creates a Decoder wrapper.
*
Expand Down Expand Up @@ -141,6 +163,16 @@ public void close() throws IOException {

/** Decodes the given data buffer starting at offset till length. */
public static byte[] decompress(byte[] data, int offset, int length) throws IOException {
return decompress(data, offset, length, new Parameters());
}

/**
* Decodes the given data buffer starting at offset till length, honoring the
* given decoder {@code params}.
*/
public static byte[] decompress(byte[] data, int offset, int length, Parameters params)
throws IOException {
int maxOutputSize = params.maxOutputSize;
ArrayList<byte[]> output = new ArrayList<>();
int totalOutputSize = 0;
DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(length);
Expand All @@ -159,6 +191,11 @@ public static byte[] decompress(byte[] data, int offset, int length) throws IOEx
buffer.get(chunk);
output.add(chunk);
totalOutputSize += chunk.length;
// BrotliDecoderTakeOutput bounds a single chunk, so this may overshoot
// by at most one chunk before aborting - enough to stop a bomb.
if (maxOutputSize > 0 && totalOutputSize > maxOutputSize) {
throw new IOException("decompressed size exceeds maximum size " + maxOutputSize);
}
break;

case NEEDS_MORE_INPUT:
Expand Down Expand Up @@ -191,6 +228,11 @@ public static byte[] decompress(byte[] data, int offset, int length) throws IOEx

/** Decodes the given data buffer. */
public static byte[] decompress(byte[] data) throws IOException {
return decompress(data, 0, data.length);
return decompress(data, 0, data.length, new Parameters());
}

/** Decodes the given data buffer honoring the given decoder {@code params}. */
public static byte[] decompress(byte[] data, Parameters params) throws IOException {
return decompress(data, 0, data.length, params);
}
}
64 changes: 64 additions & 0 deletions java/org/brotli/wrapper/dec/DecompressLimitTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* Copyright 2026 Google Inc. All Rights Reserved.

Distributed under MIT license.
See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
*/

package org.brotli.wrapper.dec;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.util.Arrays;
import org.brotli.integration.BrotliJniTestBase;
import org.brotli.wrapper.enc.Encoder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for the {@link Decoder#decompress} output-size limit. */
@RunWith(JUnit4.class)
public class DecompressLimitTest extends BrotliJniTestBase {

/** Decompression aborts once the output would exceed the limit. */
@Test
public void rejectsOutputExceedingLimit() throws IOException {
byte[] original = new byte[64 * 1024];
Arrays.fill(original, (byte) 'a');
byte[] compressed = Encoder.compress(original);

Decoder.Parameters params = new Decoder.Parameters().setMaxOutputSize(1024);
try {
Decoder.decompress(compressed, params);
fail("expected IOException for output exceeding maximum size");
} catch (IOException ex) {
assertTrue("unexpected message: " + ex.getMessage(),
ex.getMessage().contains("maximum size"));
}
}

/** Output that fits the limit is returned unchanged. */
@Test
public void allowsOutputWithinLimit() throws IOException {
byte[] original = new byte[8 * 1024];
Arrays.fill(original, (byte) 'b');
byte[] compressed = Encoder.compress(original);

Decoder.Parameters params = new Decoder.Parameters().setMaxOutputSize(original.length);
byte[] result = Decoder.decompress(compressed, params);
assertArrayEquals(original, result);
}

/** Default parameters impose no limit. */
@Test
public void unlimitedByDefault() throws IOException {
byte[] original = new byte[64 * 1024];
Arrays.fill(original, (byte) 'c');
byte[] compressed = Encoder.compress(original);

byte[] result = Decoder.decompress(compressed, new Decoder.Parameters());
assertArrayEquals(original, result);
}
}
Loading