Skip to content

Zip extraction uses archive.extract() which does not sanitize symlink entries or absolute paths #452

Description

@forkwright

Finding

extract_zip delegates extraction entirely to zip::ZipArchive::extract(output_dir) (zip crate 7.2.0). The zip crate's extract() strips leading / from entry names since zip 2.x, but it does NOT prevent symlink entries: a zip entry whose external attributes mark it as a symlink is written as a symlink file inside output_dir, with the symlink target taken from the entry content. A crafted archive can plant a symlink pointing outside output_dir (e.g. target -> ../../../etc), and subsequent nested-archive scanning (find_nested_archives) or media-import code that follows the path will traverse the symlink. No check for entry.is_symlink() or the entry.unix_mode() symlink bit exists anywhere in the extraction path.

Evidence

crates/ergasia/src/extract/zip_extract.rs:27

archive.extract(output_dir).map_err(|e| {

No sanitization of entry names or symlink bits precedes or follows this call. The post-extract enumeration at lines 36-53 uses entry.name() directly to build ExtractedFile::path, which can include symlink-resolved paths.

Why this matters

A hostile archive sourced from a malicious indexer or a poisoned torrent payload — both inside the counter-surveillance threat model — can escape the extraction sandbox. If the extracted symlink is later followed for media import or recursive extraction, it yields an arbitrary-path read or write outside the intended output directory, handing a capable adversary a foothold to read sensitive files or overwrite trusted ones on the device.

Desired correction

Before calling archive.extract(), iterate entries and abort if any entry is a symlink or carries an absolute path. Alternatively replace archive.extract() with a manual extraction loop that skips symlink entries and canonicalizes each entry path against output_dir, rejecting any that does not remain under it:

let entry_path = output_dir.join(entry.name());
if !entry_path.starts_with(output_dir) { return Err(...); }
if entry.unix_mode().map(|m| m & 0o170000 == 0o120000).unwrap_or(false) { return Err(...); }

Done when: an archive containing a symlink entry causes extract_zip to return Err rather than writing the symlink to disk.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions