Validate full-width TAR binary sizes

Combine the decoder and padding size bounds from advisory PR #1 with the checked seek offsets and sync/async regression coverage in PR #2.

Co-authored-by: 4nonym0us <2701409+4nonym0us@users.noreply.github.com>
This commit is contained in:
Adam Hathcock
2026-11-09 11:34:11 +00:00
parent 46ba719daa
commit 30586fdcc5
3 changed files with 97 additions and 12 deletions

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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<ArchiveOperationException>(async () =>
{
await using var archive = await ArchiveFactory.OpenAsyncArchive(
asyncStream,
ReaderOptions.ForExternalStream,
CancellationToken.None
);
});
}
else
{
Assert.Throws<ArchiveOperationException>(() =>
{
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;
}
}