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.
Finding
extract_zipdelegates extraction entirely tozip::ZipArchive::extract(output_dir)(zip crate 7.2.0). The zip crate'sextract()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 insideoutput_dir, with the symlink target taken from the entry content. A crafted archive can plant a symlink pointing outsideoutput_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 forentry.is_symlink()or theentry.unix_mode()symlink bit exists anywhere in the extraction path.Evidence
crates/ergasia/src/extract/zip_extract.rs:27No 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 buildExtractedFile::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 replacearchive.extract()with a manual extraction loop that skips symlink entries and canonicalizes each entry path againstoutput_dir, rejecting any that does not remain under it:Done when: an archive containing a symlink entry causes
extract_zipto returnErrrather than writing the symlink to disk.