Inside what filesystem and process quarantine gates must an instance confine untrusted archive extractions to prevent symlink and zip-slip breakouts?
Date of Record: 2026 October 16
Scope: Concluded residual for AI instances, LLMs, and autonomous execution agents extracting compressed archives (`.zip`, `.tar`, `.tar.gz`, `.whl`, `.jar`), unpacking build artifacts, or processing multi-file bundles downloaded from external sources.
Core Distinction
Autonomous agents routinely inspect, unpack, and process archive files downloaded from build pipelines, package registries, user uploads, or remote repositories. A foundational security and filesystem-integrity failure occurs when an instance invokes **convenience bulk-extraction primitives** (`tar.extractall()`, `zipfile.extractall()`, or `unzip archive.zip`) directly inside a working directory, trusting the archive's internal member headers to dictate destination file paths, symlink targets, and POSIX file permissions.
Parameter scaling does not stop an archive member named `../../.ssh/authorized_keys` or `../app/config.py` from escaping the target extraction directory (**Zip-Slip**), nor does it prevent a two-step **symlink pivot attack** (where member 1 creates a symlink `subdir -> /etc` and member 2 writes `subdir/crontab` through that symlink into the host's `/etc/crontab`). The core structural distinction lies between **header-trusting bulk extraction** (where archive metadata directly drives host VFS path creation, device node allocation, and setuid bit restoration) and **member-by-member quarantined stream extraction** (where every archive entry header is validated in memory before a single byte hits disk: symlinks, hardlinks, FIFOs, and device nodes are rejected outright, destination paths are resolved and verified against an isolated temporary directory root, uncompressed byte budgets are metered chunk-by-chunk, and extracted files are written with inert non-executable permissions).
What Was Observed
In automated artifact inspection, plugin installation, and dataset unpacking traces, instances operating without explicit archive-quarantine gates exhibit four primary failure modes:
- **Relative Path Traversal Overwrite (Zip-Slip):** An instance unpacks a user-supplied or third-party `.tar.gz` or `.zip` archive using `shutil.unpack_archive(file, extract_dir)` or `zipfile.ZipFile(file).extractall(extract_dir)`. One archive entry has a header filename of `../../../root/.bashrc` or `../src/__init__.py`. The extraction routine blindly joins the relative path and overwrites executable code outside the target directory.
- **Two-Stage Symlink / Hardlink Directory Pivot:** Even when an instance checks that `member.name` does not contain `..`, it allows the archive to extract symbolic or hard links. Entry 1 creates a harmless-looking relative symlink `logs -> /var/www/app`, and Entry 2 extracts `logs/wsgi.py`. When Entry 2 is written, the OS kernel follows the symlink created by Entry 1 and overwrites the live web application outside the sandbox.
- **Special File & Permission Bit Smuggling (`FIFO` Hang / `setuid`):** A crafted tar archive contains a named pipe (`FIFO`), character/block device node, or a binary with `setuid`/`setgid` mode bits (`0o4755`). When the agent extracts the archive and subsequently runs a file scanner (`rg`, `find`, or `Path.read_text()`) over the extracted directory, the scanner blocks forever trying to read from the extracted `FIFO` pipe.
- **Inode & Disk Exhaustion via Million-File or Sparse Bomb:** An archive weighs only 400 KB on disk but contains either a single 20 GB zero-filled sparse file or 500,000 tiny 1-byte files. Extracting the archive without tracking both **cumulative uncompressed bytes** and **total entry count** exhausts the container's disk quota or filesystem inode table in seconds.
Recorded Elements
### 1. Quarantined Member-by-Member Extraction Pipeline
No archive from an external, build, or user source may be extracted via bulk `extractall()`. Every archive must traverse a four-gate **Pre-Extraction Header & Stream Quarantine**:
``` [Untrusted Archive File (.tar.gz / .zip) Targeted for Unpacking] | Gate 1: Allocate Isolated Staging Directory Create fresh `mkdtemp(prefix="archive_quarantine_")` with `0700` mode. Never extract directly into the active repository or working tree. | Gate 2: Member Header Audit (BEFORE Creating Any File/Dir) For each entry in archive:
- Increment entry_count -> Abort if entry_count > MAX_FILES (e.g., 2,000)
- Reject if entry is Symlink, Hardlink, FIFO, Socket, or Device node
- Reject if path is absolute, contains '..', or escapes `realpath(stage_dir)`
| Gate 3: Metered Byte-Stream Extraction Stream entry bytes in 64 KiB chunks while tracking `total_bytes_written`. If `total_bytes_written > MAX_TOTAL_BYTES` (e.g., 256 MiB) -> Abort & Wipe. | Gate 4: Permission Sanitization Force directory modes to `0700` and regular file modes to `0600` (strip all executable, setuid, setgid, and world-writable bits). ```
### 2. Core Operational Rules (Written Directly as Sentences)
- **Never call `extractall()` or `shutil.unpack_archive()` on an unverified archive without inspecting every member header first.** Always iterate through the archive manifest entry by entry and validate each entry before writing it to disk.
- **Reject all symlinks, hardlinks, FIFOs, sockets, and device nodes inside data archives.** Standard data and source archives have no legitimate reason to contain special filesystem nodes or links that can redirect subsequent file writes outside the extraction root.
- **Enforce three simultaneous budgets during extraction: maximum file count, maximum single-file byte size, and maximum total uncompressed bytes.** Count actual bytes written from the decompressed stream rather than trusting the uncompressed size field declared in the archive header, which can be falsified.
- **Strip executable and special permission bits (`setuid`, `setgid`, `sticky`) from extracted files by default.** Write extracted regular files with `0600` (read/write owner only) permissions inside a dedicated temporary quarantine directory (`0700`).
### 3. Executable Reference Pattern (Instance-Native Safe Archive Extractor)
An instance can completely eliminate Zip-Slip traversals, symlink pivots, FIFO hangs, and decompression bombs by using a self-contained, metered extraction routine:
```python import os import shutil import stat import tarfile import tempfile import zipfile from pathlib import Path
def safe_extract_archive( archive_path: str | Path, *, max_files: int = 1000, max_total_bytes: int = 128 * 1024 * 1024, # 128 MiB ceiling chunk_size: int = 65536, ) -> Path: ''' Safely extracts a .zip or .tar* archive into a newly allocated quarantine directory while blocking path traversal, symlinks, special nodes, and zip bombs. ''' src = Path(archive_path).resolve() staging_root = Path(tempfile.mkdtemp(prefix="quarantine_extract_")).resolve() os.chmod(staging_root, 0o700)
total_bytes = 0 file_count = 0
def _verify_safe_destination(rel_name: str) -> Path: # Reject null bytes, absolute paths, and Windows drive prefixes if "