diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index d68e7177..7dcc4c54 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -151,7 +151,17 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory } else { - headerBytes = reader.ReadUInt32(); + try + { + headerBytes = reader.ReadUInt32(); + } + catch (EndOfStreamException ex) + { + throw new InvalidFormatException( + "Unexpected end of stream while reading ZIP archive", + ex + ); + } } _lastEntryHeader = null; diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index 053d1679..b15bb7f5 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -706,6 +706,10 @@ internal partial class CBZip2InputStream : Stream groupPos = BZip2Constants.G_SIZE; } groupPos--; + if (groupNo < 0 || groupNo >= selector.Length) + { + throw new InvalidFormatException("BZip2: group selector out of range"); + } zt = selector[groupNo]; zn = minLens[zt]; zvec = BsR(zn); @@ -788,6 +792,10 @@ internal partial class CBZip2InputStream : Stream groupPos = BZip2Constants.G_SIZE; } groupPos--; + if (groupNo < 0 || groupNo >= selector.Length) + { + throw new InvalidFormatException("BZip2: group selector out of range"); + } zt = selector[groupNo]; zn = minLens[zt]; zvec = BsR(zn); @@ -862,6 +870,10 @@ internal partial class CBZip2InputStream : Stream BlockOverrun(); } + if (nextSym - 1 < 0 || nextSym - 1 >= yy.Length) + { + throw new InvalidFormatException("BZip2: symbol out of range"); + } tmp = yy[nextSym - 1]; unzftab[seqToUnseq[tmp]]++; ll8[last] = seqToUnseq[tmp]; @@ -898,6 +910,10 @@ internal partial class CBZip2InputStream : Stream groupPos = BZip2Constants.G_SIZE; } groupPos--; + if (groupNo < 0 || groupNo >= selector.Length) + { + throw new InvalidFormatException("BZip2: group selector out of range"); + } zt = selector[groupNo]; zn = minLens[zt]; zvec = BsR(zn); diff --git a/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs b/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs index 3a7b840c..18eb27fb 100644 --- a/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs +++ b/src/SharpCompress/Compressors/Deflate64/HuffmanTree.cs @@ -244,6 +244,10 @@ internal sealed class HuffmanTree overflowBits--; } while (overflowBits != 0); + if (index < 0 || index >= array.Length) + { + throw new InvalidFormatException("Deflate64: invalid Huffman data"); + } array[index] = (short)ch; } } diff --git a/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs b/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs index 5c4149b7..db751b3a 100644 --- a/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs +++ b/src/SharpCompress/Compressors/Reduce/ReduceStream.Async.cs @@ -26,6 +26,7 @@ public partial class ReduceStream { if (inByteCount == compressedSize) { + _inputExhausted = true; return EOF; } @@ -35,6 +36,7 @@ public partial class ReduceStream .ConfigureAwait(false); if (bytesRead == 0) { + _inputExhausted = true; return EOF; } @@ -117,6 +119,13 @@ public partial class ReduceStream { if (length == 0) { + if (_inputExhausted && bitBufferCount <= 0) + { + throw new InvalidFormatException( + "ReduceStream: compressed data exhausted before uncompressed size reached" + ); + } + byte nextByte = await GetNextByteAsync(cancellationToken).ConfigureAwait(false); if (nextByte != RunLengthCode) { diff --git a/src/SharpCompress/Compressors/Reduce/ReduceStream.cs b/src/SharpCompress/Compressors/Reduce/ReduceStream.cs index 75de965c..f935619a 100644 --- a/src/SharpCompress/Compressors/Reduce/ReduceStream.cs +++ b/src/SharpCompress/Compressors/Reduce/ReduceStream.cs @@ -115,16 +115,24 @@ public partial class ReduceStream : Stream private int bitBufferCount; private ulong bitBuffer; + private bool _inputExhausted; private int NEXTBYTE() { if (inByteCount == compressedSize) { + _inputExhausted = true; return EOF; } inByteCount++; - return inStream.ReadByte(); + int b = inStream.ReadByte(); + if (b < 0) + { + _inputExhausted = true; + return EOF; + } + return b; } private void READBITS(int nbits, out byte zdest) @@ -208,6 +216,13 @@ public partial class ReduceStream : Stream { if (length == 0) { + if (_inputExhausted && bitBufferCount <= 0) + { + throw new InvalidFormatException( + "ReduceStream: compressed data exhausted before uncompressed size reached" + ); + } + byte nextByte = GetNextByte(); if (nextByte != RunLengthCode) { diff --git a/src/SharpCompress/Compressors/Shrink/ShrinkStream.Async.cs b/src/SharpCompress/Compressors/Shrink/ShrinkStream.Async.cs index 73ed2125..870be013 100644 --- a/src/SharpCompress/Compressors/Shrink/ShrinkStream.Async.cs +++ b/src/SharpCompress/Compressors/Shrink/ShrinkStream.Async.cs @@ -33,29 +33,13 @@ internal partial class ShrinkStream : Stream return; } - // Read all compressed data asynchronously - var src = new byte[_compressedSize]; - int bytesRead = 0; - int totalBytesRead = 0; - - while (totalBytesRead < (int)_compressedSize) - { - bytesRead = await inStream - .ReadAsync( - src, - totalBytesRead, - (int)_compressedSize - totalBytesRead, - cancellationToken - ) - .ConfigureAwait(false); - if (bytesRead == 0) - { - throw new IncompleteArchiveException( - "Unexpected end of stream while reading compressed data" - ); - } - totalBytesRead += bytesRead; - } + // Read actual compressed data from stream rather than pre-allocating based on the + // declared compressed size, which may be crafted to cause an OutOfMemoryException. + // The stream is already bounded by ReadOnlySubStream in ZipFilePart. + using var srcMs = new MemoryStream(); + await inStream.CopyToAsync(srcMs, cancellationToken).ConfigureAwait(false); + var src = srcMs.ToArray(); + var srcLen = src.Length; // Decompress synchronously (CPU-bound operation) var srcUsed = 0; @@ -63,7 +47,7 @@ internal partial class ShrinkStream : Stream HwUnshrink.Unshrink( src, - (int)_compressedSize, + srcLen, out srcUsed, _byteOut, (int)_uncompressedSize, diff --git a/src/SharpCompress/Compressors/Shrink/ShrinkStream.cs b/src/SharpCompress/Compressors/Shrink/ShrinkStream.cs index 97da7431..3302e419 100644 --- a/src/SharpCompress/Compressors/Shrink/ShrinkStream.cs +++ b/src/SharpCompress/Compressors/Shrink/ShrinkStream.cs @@ -8,7 +8,6 @@ internal partial class ShrinkStream : Stream { private Stream inStream; - private ulong _compressedSize; private long _uncompressedSize; private byte[] _byteOut; private long _outBytesCount; @@ -24,7 +23,6 @@ internal partial class ShrinkStream : Stream { inStream = stream; - _compressedSize = (ulong)compressedSize; _uncompressedSize = uncompressedSize; _byteOut = new byte[_uncompressedSize]; _outBytesCount = 0L; @@ -55,14 +53,19 @@ internal partial class ShrinkStream : Stream { if (!_decompressed) { - var src = new byte[_compressedSize]; - inStream.ReadExact(src, 0, (int)_compressedSize); + // Read actual compressed data from stream rather than pre-allocating based on the + // declared compressed size, which may be crafted to cause an OutOfMemoryException. + // The stream is already bounded by ReadOnlySubStream in ZipFilePart. + using var srcMs = new MemoryStream(); + inStream.CopyTo(srcMs); + var src = srcMs.ToArray(); + var srcLen = src.Length; var srcUsed = 0; var dstUsed = 0; HwUnshrink.Unshrink( src, - (int)_compressedSize, + srcLen, out srcUsed, _byteOut, (int)_uncompressedSize, diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 7d5f0448..5059aafe 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/MalformedInputTests.cs b/tests/SharpCompress.Test/MalformedInputTests.cs index 56cf5d05..d815a410 100644 --- a/tests/SharpCompress.Test/MalformedInputTests.cs +++ b/tests/SharpCompress.Test/MalformedInputTests.cs @@ -130,5 +130,50 @@ public class MalformedInputTests "504b03040a0200000e001c0068646c6c6f2e7478745554ac507578000000000000000000000000000000000000000000e80300000000000068030a0000000000147f040020303a360600002e7478745554090003a8c8b6696045ac69f5780b0006ff1d000908180000e8030000000000a4810000109a9a9a8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b9a0000000000000000000000e80300000000000068030a0000009a9a9a504b03440a6fcb486c6c6f2e74ffff" ); } + + [Fact] + public void Reduce_DecompressionBomb_Method2_ThrowsLibraryException() + { + // 31-byte ZIP using Reduce method 2 with declared uncompressed size far exceeding the + // actual compressed data - the decompressor must not generate unbounded output. + VerifyMalformedInputThrowsLibraryException( + "504b03040a000000020000000200f7ff0500f7ff05ff200600180700000000" + ); + } + + [Fact] + public void Deflate64_HuffmanTree_IndexOutOfRange_ThrowsLibraryException() + { + // 105-byte ZIP using Deflate64 with invalid Huffman code lengths causing IOOB in CreateTable + VerifyMalformedInputThrowsLibraryException( + "504b03040a00005409000088c8b669757800009ac8b66975783606000000640028b52ffd047fff" + + "02009a888888888820313735303600303132002030007573746172202000757001307230819b75" + + "72756e7475410a000c2000391eeb061ffe391eeb068f0c0a000c20" + ); + } + + [Fact] + public void BZip2_GetAndMoveToFrontDecode_IndexOutOfRange_ThrowsLibraryException() + { + // 93-byte BZip2 stream triggering IOOB deeper in GetAndMoveToFrontDecode + VerifyMalformedInputThrowsLibraryException( + "425a6839314159265359c1c080e2000001410000100244a00100808b640006000775780b2ef2ed" + + "0001393beb06060606060606060606f9050605060606060f0654090003ffffff7f003403" + + "0a0002001f8b7fff0000000000e98b8b3931" + ); + } + + [Fact] + public void Zip_ShrinkOOM_CraftedCompressedSize_ThrowsLibraryException() + { + // 122-byte ZIP with Shrink compression and compressed size set to 0x7FFFFFFF (2 GB). + // The library must not attempt to allocate a 2 GB buffer based on the untrusted header. + VerifyMalformedInputThrowsLibraryException( + "504b03040a0000000100147f6f5c20303a36ffffff7f0600000009001c0068656c6c6f2e747874" + + "5554090003a8c8b6696045ac6975780b01e8303a36060000000600000009001800000001004f2a" + + "2a2a2a0c2000395d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d000004e8303a360600000006000000" + + "0900180000" + ); + } } #endif