---
title: "Extract untrusted archives"
url: https://recursiveai.org/onearchive/cookbook/safe-extraction/
description: "Extract an archive a user uploaded with OneArchive, and see what its safe defaults refuse: a path that climbs out of the target, and an archive larger…"
version: "0.7.0-preview.1"
---

# Extract untrusted archives

Extract an archive a user uploaded with OneArchive, and see what its safe defaults refuse: a path that climbs out of the target, and an archive larger than you allow.

`upload.tar` holds one ordinary file and one entry named `../../home/user/.bashrc`, the classic “zip slip”. It was made with Python’s `tarfile`, as an attacker would:

```python
import tarfile, io
with tarfile.open('upload.tar', 'w') as t:
    for name, data in [('readme.txt', b'hello\n'), ('../../home/user/.bashrc', b'echo pwned\n')]:
        i = tarfile.TarInfo(name); i.size = len(data); t.addfile(i, io.BytesIO(data))
```

```csharp
using RecursiveAI.OneArchive;
using RecursiveAI.OneCompress;

// upload.tar came from a user: one good entry, and one whose name climbs out of the target.
using (var archive = Archive.Open("upload.tar"))
    foreach (var e in archive.Entries) Console.WriteLine($"entry: {e.Name}");

try
{
    Archive.Extract("upload.tar", "out");
}
catch (ArchiveSafetyException e)
{
    Console.WriteLine($"refused: rule {e.Rule}, entry {e.EntryName}");
}
Console.WriteLine($"files in out/: {string.Join(", ", Directory.GetFiles("out", "*", SearchOption.AllDirectories))}");

// Opting in to Sanitize drops the ".." components and keeps the file inside the target.
Archive.Extract("upload.tar", "clean", new ArchiveExtractOptions { UnsafePaths = UnsafePathHandling.Sanitize });
Console.WriteLine($"files in clean/: {string.Join(", ", Directory.GetFiles("clean", "*", SearchOption.AllDirectories).Order())}");

// Limits: a 1 KiB budget for the whole archive.
Archive.Create("big.tar").Add(new ArchiveEntry { Name = "big.bin", Kind = ArchiveEntryKind.File, Size = 65536 }, new byte[65536]).Save();
try
{
    using var big = Archive.Open("big.tar", new ArchiveOpenOptions { Limits = new ArchiveLimits { MaxTotalSize = 1024 } });
    big.Extract("limited");
}
catch (CompressionLimitException e)
{
    Console.WriteLine($"limit: {e.LimitName}: {e.Message}");
}
```

```text
entry: readme.txt
entry: ../../home/user/.bashrc
refused: rule path-traversal, entry ../../home/user/.bashrc
files in out/: out/readme.txt
files in clean/: clean/home/user/.bashrc, clean/readme.txt
limit: MaxTotalSize: tar: MaxTotalSize limit exceeded: the input needs 65536 but the limit is 1024
```

## What the defaults refuse

| Rule | Refused by default |
|---|---|
| `path-traversal` | a name that leaves the target, `..` included after `\` is read as `/` |
| `absolute-path` | a leading `/` or `\`, a UNC path, a drive letter |
| `link-escape` | a symbolic link whose target is absolute or climbs above the target |
| `link-write-through` | writing under a path component that is a symbolic link |
| `hardlink-target` | a hard link to anything but a file extracted earlier from the same archive |
| `special-file` | devices and FIFOs |
| `existing-file` | a file that already exists |

Each is an `ArchiveSafetyException` with the rule and the entry name, and each can be loosened one at a time on `ArchiveExtractOptions` (`UnsafePaths`, `Symlinks`, `HardLinks`, `SpecialFiles`, `Existing`).

## Good to know

- Entries before the bad one are already written when it is refused (`out/readme.txt` here). Extract into a fresh directory and delete it on failure.
- Limits are on `ArchiveOpenOptions.Limits`: `MaxTotalSize` (1 GiB by default), `MaxEntryCount` (100,000), `MaxPathLength`, `MaxPathDepth`, `MaxExpansionRatio` (1000) and `MaxWindowSize`. They are checked as each header is read, before any data, and a hit is a `CompressionLimitException` naming the limit, never a truncated file.
- setuid, setgid and sticky bits are dropped, and ownership isn’t restored.

All the rules are in [archives and safety](https://recursiveai.org/onearchive/docs/archive/#safety-defaults).
