mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user