diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs index 27817f96..559a27bc 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs @@ -37,6 +37,7 @@ internal sealed partial class TarHeader internal IArchiveEncoding ArchiveEncoding { get; } internal const int BLOCK_SIZE = 512; + internal const long MAX_ENTRY_SIZE = long.MaxValue - (BLOCK_SIZE - 1); // Maximum size for long name/link headers to prevent memory exhaustion attacks // This is generous enough for most real-world scenarios (32KB) @@ -631,23 +632,36 @@ internal sealed partial class TarHeader private static EntryType ReadEntryType(byte[] buffer) => (EntryType)buffer[156]; - private long ReadSize(byte[] buffer) + private static long ReadSize(byte[] buffer) { var size = - (buffer[124] & 0x80) == 0x80 // if size in binary - ? BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(0x80)) + (buffer[124] & 0x80) != 0 + ? ReadBase256Size(buffer) : ReadAsciiInt64Base8(buffer, 124, 11); - // A negative payload size can rewind seekable archives to the same header. + // Negative sizes can rewind seekable archives; oversized sizes overflow padding. // Validate here so ordinary entries and metadata use the same sync/async guard. - if (size < 0) + if (size is < 0 or > MAX_ENTRY_SIZE) { - throw new InvalidFormatException($"Tar entry size {size} cannot be negative."); + throw new InvalidFormatException($"Invalid TAR entry size: {size}."); } return size; } + private static long ReadBase256Size(byte[] buffer) + { + // Only the last eight bytes fit in a long. Do not silently truncate the + // remaining value bits, including the sign bits of GNU negative sizes. + var leadingBits = (buffer[124] & 0x7F) | buffer[125] | buffer[126] | buffer[127]; + if (leadingBits != 0) + { + throw new InvalidFormatException("TAR entry size does not fit in 64 bits."); + } + + return BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(128, 8)); + } + private static byte[] ReadBlock(BinaryReader reader) { var buffer = reader.ReadBytes(BLOCK_SIZE); diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs index 1628b95a..e72b23c1 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs @@ -59,11 +59,12 @@ internal static partial class TarHeaderFactory private static long PadTo512(long size) { - var zeros = (int)(size % 512); - if (zeros == 0) + // PAX size overrides bypass ReadSize, so validate the effective size too. + if (size is < 0 or > TarHeader.MAX_ENTRY_SIZE) { - return size; + throw new InvalidFormatException($"Invalid TAR entry size: {size}."); } - return checked(512 - zeros + size); + + return (size + 511) & ~511L; } } diff --git a/tests/SharpCompress.Test/Tar/TarSizeValidationTests.cs b/tests/SharpCompress.Test/Tar/TarSizeValidationTests.cs index 93510361..a8e8c6f6 100644 --- a/tests/SharpCompress.Test/Tar/TarSizeValidationTests.cs +++ b/tests/SharpCompress.Test/Tar/TarSizeValidationTests.cs @@ -21,11 +21,13 @@ public class TarSizeValidationTests [InlineData(-512L, false)] [InlineData(-1024L, false)] [InlineData(long.MinValue, false)] + [InlineData(long.MaxValue, false)] + [InlineData(TarHeader.MAX_ENTRY_SIZE + 1, false)] [InlineData(-1L, true)] [InlineData(-512L, true)] [InlineData(-1024L, true)] [InlineData(long.MinValue, true)] - public async Task Tar_Header_Rejects_Negative_Size(long size, bool negativeMarker) + public async Task Tar_Header_Rejects_Invalid_Size(long size, bool negativeMarker) { using var stream = new MemoryStream(CreateHeader(size, negativeMarker)); using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); @@ -41,6 +43,69 @@ public class TarSizeValidationTests ); } + [Theory] + [InlineData(TarHeader.MAX_ENTRY_SIZE - 1)] + [InlineData(TarHeader.MAX_ENTRY_SIZE)] + public async Task Tar_Header_Accepts_Maximum_Paddable_Size(long size) + { + using var stream = new MemoryStream(CreateHeader(size)); + using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); + var header = new TarHeader(new ArchiveEncoding()); + + Assert.True(header.Read(reader)); + Assert.Equal(size, header.Size); + + stream.Position = 0; + using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false); + using var asyncReader = new AsyncBinaryReader(asyncStream, leaveOpen: true); + Assert.True(await header.ReadAsync(asyncReader, cancellationToken: CancellationToken.None)); + Assert.Equal(size, header.Size); + } + + [Theory] + [InlineData(124, false)] + [InlineData(125, false)] + [InlineData(126, false)] + [InlineData(127, false)] + [InlineData(124, true)] + [InlineData(125, true)] + [InlineData(126, true)] + [InlineData(127, true)] + public async Task Tar_ArchiveFactory_Rejects_Binary_Size_With_Ignored_High_Bits( + int offset, + bool useAsync + ) + { + var header = CreateHeader(0); + // Keep the binary marker, but encode a size larger than a long can hold. + header[offset] |= 0x01; + WriteChecksum(header); + using var stream = new MemoryStream(header); + if (useAsync) + { + using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false); + await Assert.ThrowsAsync(async () => + { + await using var archive = await ArchiveFactory.OpenAsyncArchive( + asyncStream, + ReaderOptions.ForExternalStream, + CancellationToken.None + ); + }); + } + else + { + Assert.Throws(() => + { + using var archive = ArchiveFactory.OpenArchive( + stream, + ReaderOptions.ForExternalStream + ); + }); + } + Assert.True(stream.CanRead); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -262,10 +327,15 @@ public class TarSizeValidationTests BinaryPrimitives.WriteInt64BigEndian(header.AsSpan(128, 8), size); header[156] = (byte)'0'; Encoding.ASCII.GetBytes("ustar\0" + "00").CopyTo(header, 257); + WriteChecksum(header); + return header; + } + + private static void WriteChecksum(byte[] header) + { var checksum = TarHeader.RecalculateChecksum(header); Encoding .ASCII.GetBytes(Convert.ToString(checksum, 8).PadLeft(6, '0') + "\0 ") .CopyTo(header, 148); - return header; } }