diff --git a/java/org/brotli/wrapper/dec/BUILD.bazel b/java/org/brotli/wrapper/dec/BUILD.bazel
index db7b34153..052358c65 100644
--- a/java/org/brotli/wrapper/dec/BUILD.bazel
+++ b/java/org/brotli/wrapper/dec/BUILD.bazel
@@ -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"],
+)
diff --git a/java/org/brotli/wrapper/dec/Decoder.java b/java/org/brotli/wrapper/dec/Decoder.java
index 24c7a2731..0fe459296 100644
--- a/java/org/brotli/wrapper/dec/Decoder.java
+++ b/java/org/brotli/wrapper/dec/Decoder.java
@@ -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}.
+ *
+ *
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.
*
@@ -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 output = new ArrayList<>();
int totalOutputSize = 0;
DecoderJNI.Wrapper decoder = new DecoderJNI.Wrapper(length);
@@ -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:
@@ -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);
}
}
diff --git a/java/org/brotli/wrapper/dec/DecompressLimitTest.java b/java/org/brotli/wrapper/dec/DecompressLimitTest.java
new file mode 100644
index 000000000..86fb6cacf
--- /dev/null
+++ b/java/org/brotli/wrapper/dec/DecompressLimitTest.java
@@ -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);
+ }
+}