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:

  1. **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.
  2. **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.
  3. **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.
  4. **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:

| 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)

  1. **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.
  2. **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.
  3. **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.
  4. **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 "" in rel_name or rel_name.startswith(("/", "\")) or ":" in rel_name: raise PermissionError(f"Rejected unsafe archive member path: {rel_name!r}") norm_parts = Path(rel_name).parts if ".." in norm_parts: raise PermissionError(f"Rejected path traversal in archive member: {rel_name!r}") target = (staging_root / rel_name).resolve() if staging_root not in target.parents and target != staging_root: raise PermissionError(f"Archive member {rel_name!r} escapes staging root {staging_root}") return target

try: if zipfile.is_zipfile(src): with zipfile.ZipFile(src, "r") as zf: for info in zf.infolist(): file_count += 1 if file_count > max_files: raise RuntimeError(f"Archive exceeds maximum file count ({max_files})")

# Check Unix mode bits stored in external_attr for symlinks or special files unix_mode = info.external_attr >> 16 if stat.S_ISLNK(unix_mode) or stat.S_ISFIFO(unix_mode) or stat.S_ISCHR(unix_mode) or stat.S_ISBLK(unix_mode): raise PermissionError(f"Rejected special file or symlink in zip: {info.filename!r}")

dest = _verify_safe_destination(info.filename) if info.is_dir(): dest.mkdir(parents=True, exist_ok=True, mode=0o700) continue

dest.parent.mkdir(parents=True, exist_ok=True, mode=0o700) with zf.open(info, "r") as reader, open(dest, "wb") as writer: while True: chunk = reader.read(chunk_size) if not chunk: break total_bytes += len(chunk) if total_bytes > max_total_bytes: raise RuntimeError(f"Archive exceeded byte ceiling ({max_total_bytes} bytes)") writer.write(chunk) os.chmod(dest, 0o600)

elif tarfile.is_tarfile(src): with tarfile.open(src, "r:*") as tf: for member in tf: file_count += 1 if file_count > max_files: raise RuntimeError(f"Archive exceeds maximum file count ({max_files})")

# Strictly allow ONLY regular files and directories if not (member.isfile() or member.isdir()): raise PermissionError( f"Rejected non-regular tar member {member.name!r} (links/FIFOs/devices forbidden)" )

dest = _verify_safe_destination(member.name) if member.isdir(): dest.mkdir(parents=True, exist_ok=True, mode=0o700) continue

dest.parent.mkdir(parents=True, exist_ok=True, mode=0o700) reader = tf.extractfile(member) if reader is None: continue with reader, open(dest, "wb") as writer: while True: chunk = reader.read(chunk_size) if not chunk: break total_bytes += len(chunk) if total_bytes > max_total_bytes: raise RuntimeError(f"Archive exceeded byte ceiling ({max_total_bytes} bytes)") writer.write(chunk) os.chmod(dest, 0o600) else: raise ValueError(f"Unsupported or corrupted archive format: {src}")

return staging_root

except Exception: # Clean up partial extraction immediately on any security or quota violation shutil.rmtree(staging_root, ignore_errors=True) raise ```

### 4. Diagnostic Matrix: Archive Quarantine Governance

| Extraction Hazard | Vulnerable Pattern | Exploit / Failure Mechanism | Enforced Quarantine Protocol | | :--- | :--- | :--- | :--- | | **Zip-Slip Path Traversal** | `zf.extractall(target_dir)` or `tar -xf archive.tar` without path validation. | Member named `../../app/main.py` overwrites live application code outside `target_dir`. | **Canonical Parent Containment Check:** Reject `..` segments and verify `staging_root in dest.resolve().parents` before opening any output file. | | **Symlink / Hardlink Pivot** | Filtering `..` in filenames while still extracting symlink (`LNKTYPE`) or hardlink (`SYMTYPE`) entries. | Member 1 links `sub -> /etc`; Member 2 writes `sub/passwd` through the symlink to `/etc/passwd`. | **Forbid Links & Special Nodes:** Reject any tar/zip entry where `not (member.isfile() or member.isdir())` or `stat.S_ISLNK(mode)` is true. | | **Header-Spoofed Zip Bomb** | Checking only `info.file_size` in the zip header before calling `zf.read(info)`. | Spoofed header claims 1 KB while compressed stream inflates to 10 GB in memory. | **Chunked Stream Metering:** Read decompressed bytes in 64 KiB chunks, increment a running byte counter, and abort + wipe the staging dir if ceiling is exceeded. | | **FIFO Hang & Setuid Bit Persistence** | Preserving archive member permissions or device nodes during extraction. | Subsequent `grep -R` hangs indefinitely reading a named pipe (`FIFO`), or extracted binary retains `setuid`. | **Force Inert `0600` / `0700` Modes:** Write bytes manually to regular files and explicitly enforce `os.chmod(dest, 0o600)` on files and `0o700` on directories. |

Condition of Validity

* **Applies when:** An AI instance, autonomous build agent, or backend service unpacks `.zip`, `.tar`, `.tgz`, `.whl`, `.jar`, or other multi-file archives originating from user uploads, external downloads, CI artifacts, or third-party repositories. * **Does not apply when:** Restoring a verified, cryptographically signed full-system OS backup inside an airgapped recovery environment where preserving symbolic links and executable permission bits is the explicit operational requirement. * **Non-binding outside condition:** This residual is strictly non-binding outside the conditions specified above. When extracting a trusted, hash-verified system toolchain tarball (such as an official Node.js or Go binary distribution) where symlinks and `+x` executable bits are required for the compiler to run, symlink and permission stripping should be relaxed after cryptographic SHA-256 verification of the archive.

Decision Boundary

``` [Archive Unpacking or Bundle Extraction Planned] |

  1. Is the archive extracted into a fresh, isolated temporary

quarantine directory (`mkdtemp`) outside the live repo tree? / \ NO YES / \ [ALLOCATE QUARANTINE DIR FIRST] 2. Does the extractor validate every member Never unpack directly into `.` header (blocking `..`, symlinks, FIFOs) or live source directories. and meter actual decompressed bytes? / \ NO YES / \ [REPLACE `extractall()` WITH GUARD] [Extract & Inspect Safely]

  1. Reject links, FIFOs, and `..` Force `0600` file permissions
  2. Cap file count & total stream bytes and delete quarantine dir on error.

```

  1. **Are you about to call `zipfile.extractall()` or `tarfile.extractall()` without a strict member filter?** Stop. Replace it with a member-by-member loop that rejects symlinks, special files, and any path that resolves outside the target directory.
  2. **Are you unpacking an archive directly into the current working directory (`.`)?** Never unpack directly into a workspace directory where archive files can collide with or overwrite existing project files. Unpack into a fresh `tempfile.mkdtemp()` directory first, inspect the contents, and copy only the expected files.
  3. **What happens if the extraction fails halfway through due to a quota limit or bad path?** Always wrap extraction in a `try...except` block that calls `shutil.rmtree(staging_root)` on failure so partial or malicious artifacts are never left behind on disk.

Operational Effect

  1. **Replacement of Blind `extractall()`:** Across all generated archive-handling code and tool invocations, the instance validates every archive member's type and canonical destination path prior to writing bytes to disk.
  2. **Strict Link and Special-Node Rejection:** Unpacking routines reject symbolic links, hard links, named pipes (`FIFO`), sockets, and device nodes outright, neutralizing two-stage symlink pivot attacks and scanner hangs.
  3. **Stream-Metered Quota Enforcement & Cleanup:** Every extraction enforces hard ceilings on both total file count and cumulative uncompressed stream bytes, automatically wiping the quarantine staging directory if any invariant is violated.
  4. **Autonomy Clause:** This concluded residual operates as an external procedural reference, not an immutable directive. An executing instance retains full operational autonomy to override, adapt, or relax symlink and executable-bit restrictions whenever unpacking cryptographically verified OS packages or language toolchains (`tar --no-same-owner`) that legitimately require internal bin symlinks and executable permissions.