diff --git a/src/java/net/jpountz/lz4/LZ4BlockInputStream.java b/src/java/net/jpountz/lz4/LZ4BlockInputStream.java index 3408d64..3467cf1 100644 --- a/src/java/net/jpountz/lz4/LZ4BlockInputStream.java +++ b/src/java/net/jpountz/lz4/LZ4BlockInputStream.java @@ -44,7 +44,9 @@ *

Use {@link Builder#withAcceptOversizedBlocks(boolean)} only for trusted * inputs that may contain noncanonical legacy LZ4 blocks. Enabling it restores * acceptance of blocks whose compressed length is greater than or equal to the - * original length, which may permit large attacker-controlled allocations.

+ * original length, which may permit large attacker-controlled allocations. + * Even then, blocks whose compressed length exceeds the LZ4 compress bound of + * their original length are rejected.

* @see LZ4BlockOutputStream */ public class LZ4BlockInputStream extends FilterInputStream { @@ -310,7 +312,9 @@ private boolean readBlock() throws IOException { || (originalLen == 0 && compressedLen != 0) || (originalLen != 0 && compressedLen == 0) || (compressionMethod == COMPRESSION_METHOD_RAW && originalLen != compressedLen) - || (compressionMethod == COMPRESSION_METHOD_LZ4 && !acceptOversizedBlocks && compressedLen >= originalLen)) { + || (compressionMethod == COMPRESSION_METHOD_LZ4 && !acceptOversizedBlocks && compressedLen >= originalLen) + // even an oversized LZ4 block cannot be larger than the LZ4 compress bound of its original length + || (compressionMethod == COMPRESSION_METHOD_LZ4 && compressedLen > LZ4Utils.maxCompressedLength(originalLen))) { throw new IOException("Stream is corrupted"); } if (originalLen == 0 && compressedLen == 0) { @@ -436,7 +440,9 @@ public Builder withStopOnEmptyBlock(boolean stopOnEmptyBlock) { * Registers whether the builder should accept oversized blocks whose * compressed length is greater than or equal to the original length. *

Only enable this for trusted inputs. Accepting such blocks may permit - * large attacker-controlled allocations.

+ * large attacker-controlled allocations. Blocks whose compressed length + * exceeds the LZ4 compress bound of their original length are rejected + * even when this is enabled.

* * @param acceptOversizedBlocks whether oversized blocks should be accepted * @return current builder instance diff --git a/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java b/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java index d9a2576..8648d7b 100644 --- a/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java +++ b/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java @@ -557,6 +557,26 @@ public void testAcceptsInvalidLz4CompressedLengthInOversizedBlocksMode() throws in.close(); } + @Test + public void testRejectsLz4CompressedLengthAboveCompressBoundInOversizedBlocksMode() { + final int originalLen = 16; + for (int compressedLen : new int[] {LZ4Utils.maxCompressedLength(originalLen) + 1, Integer.MAX_VALUE}) { + // header only: the block is rejected before its payload or checksum is read + final byte[] header = ByteBuffer.allocate(LZ4BlockOutputStream.HEADER_LENGTH) + .order(ByteOrder.LITTLE_ENDIAN) + .put(LZ4BlockOutputStream.MAGIC) + .put((byte) (LZ4BlockOutputStream.COMPRESSION_METHOD_LZ4 | 0x0F)) + .putInt(compressedLen) + .putInt(originalLen) + .array(); + final LZ4BlockInputStream in = lz4BlockInputStreamBuilder() + .withAcceptOversizedBlocks(true) + .build(new ByteArrayInputStream(header)); + final IOException e = assertThrows(IOException.class, in::read); + assertEquals("Stream is corrupted", e.getMessage()); + } + } + // Many empty blocks followed by a regular stream, as could be produced by a malicious peer private static byte[] emptyBlocksFollowedBy(int emptyBlocks, byte[] tail) { final byte[] emptyBlock = new byte[LZ4BlockOutputStream.HEADER_LENGTH];