Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Formats.Tar;
using System.IO.Compression;
// <SafeExtractEntry>
void SafeExtractEntry(ZipArchiveEntry entry, string destinationPath, long maxDecompressedSize)

Check warning on line 4 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'SafeExtractEntry' is declared but never used

Check warning on line 4 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'SafeExtractEntry' is declared but never used
{
// The runtime enforces that entry.Open() will never produce more than
// entry.Length bytes, so checking the declared size is sufficient.
Expand All @@ -16,7 +16,7 @@
// </SafeExtractEntry>

// <SafeExtractArchive>
void SafeExtractArchive(ZipArchive archive, string destinationDir,

Check warning on line 19 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'SafeExtractArchive' is declared but never used
long maxTotalSize, int maxEntryCount)
{
// Flat zip bombs can contain many entries that each expand to large sizes.
Expand All @@ -41,7 +41,7 @@
// </SafeExtractArchive>

// <PathValidation>
void ValidatePaths(ZipArchive archive, string destinationDir)

Check warning on line 44 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'ValidatePaths' is declared but never used
{
string fullDestDir = Path.GetFullPath(destinationDir);
if (!fullDestDir.EndsWith(Path.DirectorySeparatorChar))
Expand All @@ -59,7 +59,7 @@
// </PathValidation>

// <VulnerablePattern>
void DangerousExtract(string extractDir)

Check warning on line 62 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'DangerousExtract' is declared but never used
{
// ⚠️ DANGEROUS: entry.FullName could contain "../" sequences
using ZipArchive archive = ZipFile.OpenRead("archive.zip");
Expand All @@ -79,7 +79,7 @@
}

// <SafeExtractZip>
void SafeExtractZip(string archivePath, string destinationDir,

Check warning on line 82 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'SafeExtractZip' is declared but never used
long maxTotalSize, long maxEntrySize, int maxEntryCount)
{
// Resolve the destination to an absolute path and ensure it ends with a
Expand Down Expand Up @@ -149,7 +149,7 @@
// </SafeExtractZip>

// <SafeExtractTar>
void SafeExtractTar(Stream archiveStream, string destinationDir,

Check warning on line 152 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'SafeExtractTar' is declared but never used
long maxTotalSize, long maxEntrySize, int maxEntryCount)
{
// Same trailing-separator technique as the ZIP example.
Expand Down Expand Up @@ -244,7 +244,7 @@
// </SafeExtractTar>

// <ValidateSymlink>
bool IsLinkTargetSafe(TarEntry entry, string fullDestDir)

Check warning on line 247 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'IsLinkTargetSafe' is declared but never used
{
// A symlink with an absolute (rooted) target is resolved from the filesystem root, not from the extraction directory.
if (Path.IsPathRooted(entry.LinkName))
Expand Down Expand Up @@ -273,7 +273,7 @@
// </ValidateSymlink>

// <StreamingApproach>
void StreamingModify()

Check warning on line 276 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'StreamingModify' is declared but never used
{
// ✅ Streaming approach for large archives
using var input = new ZipArchive(File.OpenRead("large.zip"), ZipArchiveMode.Read);
Expand All @@ -295,24 +295,29 @@
// </StreamingApproach>

// <TarStreaming>
void TarStreamingRead(Stream archiveStream, string destDir)

Check warning on line 298 in docs/standard/io/snippets/zip-tar-best-practices/csharp/Program.cs

View workflow job for this annotation

GitHub Actions / snippets-build

The local function 'TarStreamingRead' is declared but never used
{
using var reader = new TarReader(archiveStream);
TarEntry? entry;
while ((entry = reader.GetNextEntry()) is not null)
{
// DataStream is only valid until the next GetNextEntry() call,
// so consume or copy the data before advancing.
if (entry.DataStream is not null)
{
// DataStream is only valid until the next GetNextEntry() call,
// so consume
string destPath = Path.Join(destDir, entry.Name);
using var fileStream = File.Create(destPath);
entry.DataStream.CopyTo(fileStream);

// Alternatively, you can copy the entry contents into
// in a separate MemoryStream that remains valid after advancing:
if (entry.Length < 1_000_000) // Example limit
{
MemoryStream memoryStream = new MemoryStream();
Comment thread
rzikm marked this conversation as resolved.
entry.DataStream.CopyTo(memoryStream);
// memoryStream can be used after GetNextEntry() is called again
}
}
}

// Alternatively, pass copyContents: true to retain entry data
// in a separate MemoryStream that remains valid after advancing:
// entry = reader.GetNextEntry(copyContents: true);
}
// </TarStreaming>
16 changes: 7 additions & 9 deletions docs/standard/io/zip-tar-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Best for: simple workflows with trusted input, quick scripts, and build tooling.

Use these APIs for full control over each archive entry. They're essential for large archives or untrusted input.

- **ZIP:** Use <xref:System.IO.Compression.ZipArchive?displayProperty=fullName> to open an archive and iterate, read, or write entries selectively. Use <xref:System.IO.Compression.ZipFileExtensions.ExtractToFile*> to extract individual entries, or <xref:System.IO.Compression.ZipFileExtensions.ExtractToDirectory*> to extract all entries from an already-opened archive.
- **ZIP:** Use <xref:System.IO.Compression.ZipArchive?displayProperty=fullName> to open an archive and iterate, read, or write entries selectively. Use <xref:System.IO.Compression.ZipFileExtensions.ExtractToFile*> to extract individual entries.

- **TAR:** Use <xref:System.Formats.Tar.TarReader?displayProperty=fullName> and <xref:System.Formats.Tar.TarWriter> for sequential entry-by-entry access. Use <xref:System.Formats.Tar.TarEntry.ExtractToFile*?displayProperty=fullName> to extract individual entries.

Expand All @@ -75,25 +75,20 @@ When the archive source is known and trusted, the [convenience methods](#conveni

- TAR extraction handles overwriting differently: it deletes the existing file before writing the replacement. If extraction fails after deletion (for example, due to an I/O error or process interruption), the original file is lost and the replacement might be incomplete. Consider backing up critical files before overwriting with TAR extraction.

> [!NOTE]
> The convenience methods don't enforce size limits, entry count limits, or other policies needed for safe extraction of untrusted archives. If that matters even for trusted input (for example, very large archives), use the streaming approach described in [Handle untrusted archives safely](#handle-untrusted-archives-safely).
> [!WARNING]
> The `ExtractToDirectory` convenience methods must only be used on trusted inputs. These helpers don't enforce size limits, entry count limits, or other policies needed for safe extraction of untrusted archives. If that matters even for trusted input (for example, very large archives), use the streaming approach described in [Handle untrusted archives safely](#handle-untrusted-archives-safely).

## Handle untrusted archives safely

For untrusted input—user uploads, third-party downloads, or network transfers—iterate over entries manually and enforce your own safety checks. The following subsections describe what you need to enforce and why.

- [What the convenience methods don't protect you from](#what-the-convenience-methods-dont-protect-you-from)
- [Enforce size and entry count limits](#enforce-size-and-entry-count-limits)
- [Validate file names](#validate-file-names)
- [Validate destination paths](#validate-destination-paths)
- [Handle symbolic and hard links (TAR)](#handle-symbolic-and-hard-links-tar)
- [Entry permission bits (Unix only)](#entry-permission-bits-unix-only)
- [Complete safe extraction examples](#complete-safe-extraction-examples)

### What the convenience methods don't protect you from

`ExtractToDirectory` protects against *path traversal*—an attack where a malicious entry name like `../../etc/passwd` tries to write outside the destination directory. The method resolves each entry's full path and rejects any that fall outside the target directory (for TAR, this check also covers symbolic link targets). However, `ExtractToDirectory` doesn't enforce size limits or entry count limits.

### Enforce size and entry count limits

Neither <xref:System.IO.Compression.ZipArchive> nor <xref:System.Formats.Tar.TarReader> limits the total uncompressed size or the number of entries extracted, and neither do the `ExtractToDirectory` convenience methods. You must enforce these limits yourself.
Expand Down Expand Up @@ -211,7 +206,10 @@ Additionally, when you open a <xref:System.IO.Compression.ZipArchive> in <xref:S

### TAR streaming model

<xref:System.Formats.Tar.TarReader?displayProperty=fullName> reads entries one at a time and doesn't buffer the entire archive. However, for unseekable streams, each entry's <xref:System.Formats.Tar.TarEntry.DataStream?displayProperty=nameWithType> is only valid until the next <xref:System.Formats.Tar.TarReader.GetNextEntry*?displayProperty=nameWithType> call. If you need to retain entry data, either copy it immediately or pass `copyContents: true` to <xref:System.Formats.Tar.TarReader.GetNextEntry*?displayProperty=nameWithType>, which copies the entry data into a separate <xref:System.IO.MemoryStream> that remains valid after advancing. Like <xref:System.IO.Compression.ZipArchiveMode.Update?displayProperty=nameWithType>, `copyContents: true` loads the full entry into memory, so check entry sizes before using it with untrusted archives.
<xref:System.Formats.Tar.TarReader?displayProperty=fullName> reads entries one at a time and doesn't buffer the entire archive. However, for unseekable streams, each entry's <xref:System.Formats.Tar.TarEntry.DataStream?displayProperty=nameWithType> is only valid until the next <xref:System.Formats.Tar.TarReader.GetNextEntry*?displayProperty=nameWithType> call. If you need to retain entry data, copy it immediately to a separate <xref:System.IO.MemoryStream> that remains valid after advancing.

> [!WARNING]
> Avoid using <xref:System.Formats.Tar.TarReader.GetNextEntry*?displayProperty=nameWithType> with `copyContents: true` on untrusted archives, as it allocates a potentially large amount of memory for the <xref:System.IO.MemoryStream> to hold the entry contents. Pass `copyContents: false` and validate the entry size before materializing the contents manually.

:::code language="csharp" source="./snippets/zip-tar-best-practices/csharp/Program.cs" id="TarStreaming":::

Expand Down
Loading