diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs index ee5bd67e21f65c..9e6c5ba913efae 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.cs @@ -257,9 +257,18 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e // 2. When the size indicates that all the information is available ("slightly invalid files"). bool readAllFields = extraField.Size >= MaximumExtraFieldLength; + // The original values are unsigned 64-bit, so a negative signed value means the + // value does not fit in Int64 and cannot be represented by the rest of the API + // (which uses long). Validate each field as it is read so that short extra fields + // (which exit early below) cannot bypass the check. + if (readUncompressedSize) { zip64Block._uncompressedSize = BinaryPrimitives.ReadInt64LittleEndian(data); + if (zip64Block._uncompressedSize < 0) + { + throw new InvalidDataException(SR.FieldTooBigUncompressedSize); + } data = data.Slice(FieldLengths.UncompressedSize); } else if (readAllFields) @@ -275,6 +284,10 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e if (readCompressedSize) { zip64Block._compressedSize = BinaryPrimitives.ReadInt64LittleEndian(data); + if (zip64Block._compressedSize < 0) + { + throw new InvalidDataException(SR.FieldTooBigCompressedSize); + } data = data.Slice(FieldLengths.CompressedSize); } else if (readAllFields) @@ -290,6 +303,10 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e if (readLocalHeaderOffset) { zip64Block._localHeaderOffset = BinaryPrimitives.ReadInt64LittleEndian(data); + if (zip64Block._localHeaderOffset < 0) + { + throw new InvalidDataException(SR.FieldTooBigLocalHeaderOffset); + } data = data.Slice(FieldLengths.LocalHeaderOffset); } else if (readAllFields) @@ -307,20 +324,6 @@ private static bool TryGetZip64BlockFromGenericExtraField(ZipGenericExtraField e zip64Block._startDiskNumber = BinaryPrimitives.ReadUInt32LittleEndian(data); } - // original values are unsigned, so implies value is too big to fit in signed integer - if (zip64Block._uncompressedSize < 0) - { - throw new InvalidDataException(SR.FieldTooBigUncompressedSize); - } - if (zip64Block._compressedSize < 0) - { - throw new InvalidDataException(SR.FieldTooBigCompressedSize); - } - if (zip64Block._localHeaderOffset < 0) - { - throw new InvalidDataException(SR.FieldTooBigLocalHeaderOffset); - } - return true; } diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs index 46d899426459e8..f21623e2e6b903 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -2434,5 +2434,133 @@ public static async Task OpenWithFileAccess_DisposedArchive_Throws(bool async) Assert.Throws(() => entry.Open(FileAccess.Read)); await Assert.ThrowsAsync(() => entry.OpenAsync(FileAccess.Read)); } + + [Theory] + [MemberData(nameof(Get_Booleans_Data))] + public static async Task Zip64ExtraField_NegativeUncompressedSize_Throws(bool async) + { + // A ZIP64 extra field that encodes the uncompressed size as 0xFFFF_FFFF_FFFF_FFFF + // (observed as -1L) cannot be represented by the long-based public surface and is + // malformed. The central directory parser must reject it eagerly so that callers + // never see a negative Length or CompressedLength on a ZipArchiveEntry. + byte[] zipArchive = CreateZipWithNegativeZip64UncompressedSize(); + + await Assert.ThrowsAsync(async () => + { + ZipArchive archive = await CreateZipArchive(async, new MemoryStream(zipArchive), ZipArchiveMode.Read); + await DisposeZipArchive(async, archive); + }); + } + + private static byte[] CreateZipWithNegativeZip64UncompressedSize() + { + // Crafts a minimal ZIP whose central directory entry uses the ZIP64 sentinel + // (0xFFFFFFFF) for both compressed and uncompressed size, while the matching + // ZIP64 extra field stores -1L (0xFFFFFFFFFFFFFFFF) as the 64-bit uncompressed + // size. The local file header uses the same shape so the file is internally + // consistent up to the malformed size value. + // + // Layout: + // Local file header + ZIP64 extra (uncompressed=-1, compressed=0) + // (no file data) + // Central directory header + ZIP64 extra (uncompressed=-1, compressed=0) + // End of central directory record + using MemoryStream ms = new MemoryStream(); + + static void WriteUInt16(Stream stream, ushort value) + { + Span buffer = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); + stream.Write(buffer); + } + + static void WriteUInt32(Stream stream, uint value) + { + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + stream.Write(buffer); + } + + static void WriteInt64(Stream stream, long value) + { + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, value); + stream.Write(buffer); + } + + const uint LocalFileHeaderSig = 0x04034b50; + const uint CentralDirSig = 0x02014b50; + const uint EndCentralDirSig = 0x06054b50; + const ushort VersionNeeded = 45; + const ushort VersionMadeBy = 45; + const ushort GeneralPurposeBitFlag = 0; + const ushort CompressionMethod = 0; + const ushort LastModFileTime = 0; + const ushort LastModFileDate = 0; + const uint Crc32 = 0; + const uint SentinelSize = 0xFFFFFFFF; + const ushort Zip64Tag = 1; + const ushort Zip64ExtraDataSize = 16; + const ushort Zip64ExtraTotalSize = 4 + Zip64ExtraDataSize; + const long Zip64UncompressedSize = -1; + const long Zip64CompressedSize = 0; + + byte[] fileNameBytes = Encoding.UTF8.GetBytes("test.txt"); + + WriteUInt32(ms, LocalFileHeaderSig); + WriteUInt16(ms, VersionNeeded); + WriteUInt16(ms, GeneralPurposeBitFlag); + WriteUInt16(ms, CompressionMethod); + WriteUInt16(ms, LastModFileTime); + WriteUInt16(ms, LastModFileDate); + WriteUInt32(ms, Crc32); + WriteUInt32(ms, SentinelSize); + WriteUInt32(ms, SentinelSize); + WriteUInt16(ms, (ushort)fileNameBytes.Length); + WriteUInt16(ms, Zip64ExtraTotalSize); + ms.Write(fileNameBytes, 0, fileNameBytes.Length); + WriteUInt16(ms, Zip64Tag); + WriteUInt16(ms, Zip64ExtraDataSize); + WriteInt64(ms, Zip64UncompressedSize); + WriteInt64(ms, Zip64CompressedSize); + + long centralDirectoryOffset = ms.Position; + + WriteUInt32(ms, CentralDirSig); + WriteUInt16(ms, VersionMadeBy); + WriteUInt16(ms, VersionNeeded); + WriteUInt16(ms, GeneralPurposeBitFlag); + WriteUInt16(ms, CompressionMethod); + WriteUInt16(ms, LastModFileTime); + WriteUInt16(ms, LastModFileDate); + WriteUInt32(ms, Crc32); + WriteUInt32(ms, SentinelSize); + WriteUInt32(ms, SentinelSize); + WriteUInt16(ms, (ushort)fileNameBytes.Length); + WriteUInt16(ms, Zip64ExtraTotalSize); + WriteUInt16(ms, 0); + WriteUInt16(ms, 0); + WriteUInt16(ms, 0); + WriteUInt32(ms, 0); + WriteUInt32(ms, 0); + ms.Write(fileNameBytes, 0, fileNameBytes.Length); + WriteUInt16(ms, Zip64Tag); + WriteUInt16(ms, Zip64ExtraDataSize); + WriteInt64(ms, Zip64UncompressedSize); + WriteInt64(ms, Zip64CompressedSize); + + long centralDirSize = ms.Position - centralDirectoryOffset; + + WriteUInt32(ms, EndCentralDirSig); + WriteUInt16(ms, 0); + WriteUInt16(ms, 0); + WriteUInt16(ms, 1); + WriteUInt16(ms, 1); + WriteUInt32(ms, (uint)centralDirSize); + WriteUInt32(ms, (uint)centralDirectoryOffset); + WriteUInt16(ms, 0); + + return ms.ToArray(); + } } }