Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ public async ValueTask<ArchiveDatabase> ReadDatabaseAsync(
throw new ArchiveOperationException("nextHeaderOffset is invalid");
}

// Validate the physical header range before allocating attacker-controlled sizes.
// Subtract the validated offset instead of adding offset and size to avoid overflow.
if (nextHeaderSize > _streamEnding - db._startPositionAfterHeader - nextHeaderOffset)
{
throw new IncompleteArchiveException("Next header extends beyond the end of stream.");
}

_stream.Seek(nextHeaderOffset, SeekOrigin.Current);

var header = new byte[nextHeaderSize];
Expand Down
7 changes: 7 additions & 0 deletions src/SharpCompress/Common/SevenZip/ArchiveReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,13 @@ public ArchiveDatabase ReadDatabase(IPasswordProvider pass)
throw new ArchiveOperationException("nextHeaderOffset is invalid");
}

// Validate the physical header range before allocating attacker-controlled sizes.
// Subtract the validated offset instead of adding offset and size to avoid overflow.
if (nextHeaderSize > _streamEnding - db._startPositionAfterHeader - nextHeaderOffset)
{
throw new IncompleteArchiveException("Next header extends beyond the end of stream.");
}

_stream.Seek(nextHeaderOffset, SeekOrigin.Current);

var header = new byte[nextHeaderSize];
Expand Down
128 changes: 128 additions & 0 deletions tests/SharpCompress.Test/SevenZip/SevenZipHeaderValidationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Common.SevenZip;
using SharpCompress.Crypto;
using SharpCompress.Test.Mocks;
using Xunit;

namespace SharpCompress.Test.SevenZip;

public class SevenZipHeaderValidationTests
{
[Theory]
[InlineData(0, 64 * 1024 * 1024, 0)]
[InlineData(0, 256 * 1024 * 1024, 0)]
[InlineData(0, 2, 1)]
[InlineData(1, 2, 2)]
[InlineData(2, 1, 2)]
public void SevenZipArchive_TruncatedNextHeader_ThrowsBeforeReadingHeader(
int offset,
int size,
int available
)
{
using var stream = CreateArchive(offset, size, available);
using var archive = ArchiveFactory.OpenArchive(stream);

var exception = Assert.Throws<IncompleteArchiveException>(() => archive.Entries.ToList());

Assert.Equal("Next header extends beyond the end of stream.", exception.Message);
Assert.Equal(32L, stream.Position);
Assert.True(stream.CanRead);
}

[Theory]
[InlineData(0, 64 * 1024 * 1024, 0)]
[InlineData(0, 256 * 1024 * 1024, 0)]
[InlineData(0, 2, 1)]
[InlineData(1, 2, 2)]
[InlineData(2, 1, 2)]
public async Task SevenZipArchive_TruncatedNextHeader_AsyncThrowsBeforeReadingHeader(
int offset,
int size,
int available
)
{
using var stream = CreateArchive(offset, size, available);
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
await using var archive = await ArchiveFactory.OpenAsyncArchive(
asyncStream,
cancellationToken: CancellationToken.None
);

var exception = await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
{
await foreach (
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
) { }
});

Assert.Equal("Next header extends beyond the end of stream.", exception.Message);
Assert.Equal(32L, stream.Position);
Assert.True(stream.CanRead);
}

[Theory]
[InlineData(0, 0, 0)]
[InlineData(0, 2, 2)]
[InlineData(3, 2, 5)]
public void SevenZipArchive_NextHeaderAtEnd_IsValid(int offset, int size, int available)
{
using var stream = CreateArchive(offset, size, available);
using var archive = ArchiveFactory.OpenArchive(stream);

Assert.Empty(archive.Entries);
}

[Theory]
[InlineData(0, 0, 0)]
[InlineData(0, 2, 2)]
[InlineData(3, 2, 5)]
public async Task SevenZipArchive_NextHeaderAtEnd_AsyncIsValid(
int offset,
int size,
int available
)
{
using var stream = CreateArchive(offset, size, available);
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
await using var archive = await ArchiveFactory.OpenAsyncArchive(
asyncStream,
cancellationToken: CancellationToken.None
);

var count = 0;
await foreach (var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None))
{
count++;
}

Assert.Equal(0, count);
}

private static MemoryStream CreateArchive(int offset, int size, int available)
{
// A minimal uncompressed next header: kHeader followed by kEnd.
byte[] header = [1, 0];
var crc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
header
);
var stream = new MemoryStream();
SevenZipSignatureHeaderWriter.WriteFinal(stream, (ulong)offset, (ulong)size, crc);
stream.SetLength(32 + available);
if (available >= offset + header.Length)
{
stream.Position = 32 + offset;
stream.Write(header, 0, header.Length);
}

stream.Position = 0;
return stream;
}
}
Loading