To review an archive extractor, prove that every member can create data only inside a fresh, intended destination, even after filename parsing, links, duplicate names, and filesystem behavior are considered. Then check that extraction cannot exhaust disk, memory, file count, or processing time. A ZIP or TAR extension does not make contained paths trustworthy. A library's “extract all” helper may have safeguards, but those safeguards vary by format, API and version; a custom loop that joins a member name to a directory may bypass them. The conclusion changes if the archive is fully trusted and never shares a path with user-controlled content, but most customer uploads and downloaded build artifacts should be treated as untrusted input.
This is a specific path traversal review. The destination is not just a string prefix: it is the filesystem location where the operating system will create or overwrite files. Python's ZIP documentation warns against extracting untrusted archives without inspection and notes that zipfile.Path does not sanitize member names for callers. The TAR documentation explains both extraction filters and their remaining limits. Read the documentation for the exact library and runtime in your application.
Trace an import from archive to final use
Illustrative codebase and paths are invented. Atlas Reports accepts a ZIP export from a customer and imports reports/ files into a private staging area. The worker iterates over entries, builds staging_root / entry.name, and writes each entry. The scanner labels this “Zip Slip.” The risky operation is visible, but the review still needs to determine which member names are accepted, whether the staging directory is fresh, whether links can appear, and whether anything moves from staging into a live application path.
The desired outcome is narrow: accepted regular files under reports/ become parsed report records. No archive member chooses a path outside the import's private staging directory, and no extracted file is executed as application code. Draw this flow:
Illustrative review flow; no hostile archive is created or extracted here.
customer upload -> ZIP reader -> member metadata -> path decision
| |
file/link type? inside new root?
|
bounded streaming write
|
report parser -> records
The last arrow matters. Even perfectly contained extraction can feed a dangerous parser or public file server. This article focuses on path safety and extraction limits; the importer must also review the format of the files it later consumes.
Inspect the path decision at the write point
Start with the exact API and version. Is the application calling a library extraction method, using a virtual archive path API, or manually writing open(destination / member_name)? A validator attached to the upload route does not help if a later worker uses raw member names. A check after writing is too late. Record the path decision made immediately before each filesystem operation.
The following is policy pseudocode, not a drop-in extractor. It shows the order of the defensive decisions:
root = new_private_empty_directory()
for member in archive:
reject unless member is an expected regular file or directory
reject absolute, drive-qualified, parent, empty, or ambiguous path parts
reject links and special files unless a separate policy needs and checks them
destination = resolve_under(root, member.name)
reject unless destination is strictly inside root
reject duplicate or case-colliding destination names
if directory: create only the constrained directory and continue
if regular file: create exclusively and stream, counting actual expanded bytes
stop if per-file, total-byte, entry-count, depth, or time limit is reached
publish parsed records only after the entire import succeeds
“Strictly inside” means comparing resolved path components, not testing whether a string begins with the root text. A root named /imports/job is a string prefix of /imports/job-old; it is not the same directory. Also check the filesystem between the decision and the write. If another process can replace a path component with a link, a correct string check may still write elsewhere. The simplest starting point is a newly created private directory that no attacker or parallel task can modify. Where a shared directory is unavoidable, use appropriate no-follow, directory-relative filesystem operations and review race behavior with a platform specialist.
For ZIP imports, the name displayed by a library may already have undergone decoding or normalization. Reject names your application cannot interpret consistently, including alternate separators on the deployment platform. Check whether two names become the same filesystem path after case folding or normalization. A duplicate later member may silently overwrite an earlier one. Atlas Reports has no business need for links, devices or permissions from the archive, so its policy should reject them rather than reproduce their filesystem semantics.
Do not mistake a library safeguard for the whole policy
Python's ZipFile.extract() and extractall() attempt to remove some absolute and parent path components, while zipfile.Path leaves filename sanitation to its caller. The documented warning still says to inspect untrusted archives before extraction. A reviewer should therefore identify the actual method, not write “Python protects ZIP” or “Python ZIP is unsafe” without context.
For TAR, link and special-file semantics are broader. The current Python tarfile documentation says the data extraction filter is the default from Python 3.14 and blocks some dangerous path and link cases, but also states that this filter alone does not make untrusted extraction safe. Older runtimes or downstream distributions may differ, and the documentation recommends checking for the filter capability rather than assuming it from a version number. If Atlas also accepts TAR, review that path separately; do not infer its behavior from the ZIP handler.
Path safety also does not address expansion. A small compressed upload can produce many entries or substantial extracted content. File sizes in archive metadata help plan limits but the worker must count actual streamed bytes. Limit the number of entries, nesting depth, individual size, total expanded size, CPU time and temporary disk use according to the import's legitimate maximum. Fail closed and remove the private staging area if any bound is exceeded. A report that says “path traversal prevented” should not be read as “archive processing is safe under all resource conditions.”
Review a manifest before imagining an exploit
Atlas's import policy accepts regular report files beneath reports/, rejects links and duplicates, and stages the whole import privately before publishing records. Consider this invented manifest:
| Member in archive order | Policy decision | Reason |
|---|---|---|
reports/january.json regular file |
Eligible | Intended report namespace |
reports/february.json regular file |
Eligible | Distinct intended record |
Second reports/january.json |
Reject archive | Would make later data overwrite earlier data ambiguously |
notes/readme.txt |
Reject archive | Outside this import's supported namespace, even if inside staging |
reports/current link entry |
Reject archive | Product has no need to reproduce links |
This is a review fixture, not a created archive. The duplicate is interesting because it need not escape the root to violate the import contract. If preview inspected the first January record while extraction kept the second, approval and ingestion would refer to different content. Track canonical destination identities, not only original member strings. Case-insensitive filesystems and normalization can make differently spelled names collide; the deployed filesystem and library decide which equivalences matter.
A rejection after two files were staged should still publish zero report records. The reader needs to find the commit boundary separating private extracted data from durable imported records. A loop that inserts each report immediately cannot satisfy an all-or-error policy merely by deleting the staging directory afterward.
Count the work as it happens
For a small illustrative fixture, set a total expanded-byte cap of 4 MiB and a per-file cap of 3 MiB. These are teaching values, not recommended production limits. January expands to 2 MiB and February to 1.5 MiB, leaving 0.5 MiB of total allowance. A third file requesting another 1 MiB must stop before writing beyond that remaining allowance.
total written: 3.5 MiB
next available output chunk: 1 MiB
remaining total allowance: 0.5 MiB
decision: reject before writing the full chunk; publish zero records
The extractor may need bounded reads and an external resource boundary because a decompressor can allocate or consume CPU before the application sees an output chunk. Header size claims are not enforcement. Entry count, path depth, temporary disk and elapsed work also need independent bounds. A nested archive remains an ordinary unsupported file unless the product intentionally admits another extraction stage with a shared budget.
A library upgrade changes one part of the decision
Suppose the deployed TAR worker moves to a runtime using Python's data filter by default. The current documentation describes the Python 3.14 default and remaining limits. Record the actual capability/configuration; do not carry an old “fully trusted default” finding forward without checking it. But the upgrade does not choose Atlas's duplicate-name policy, stop partial database publication or establish the 4 MiB work budget.
The source-only handoff becomes: “R32 uses a fresh private root and explicit member policy. Duplicate canonical destinations reject the import, expanded work is bounded, and records become visible only after complete acceptance. The runtime filter capability, actual open confinement and external resource termination still require selected-deployment evidence.” If a manual write loop bypasses the library's extraction API, its own path decision remains in scope regardless of the runtime upgrade.
An authorized owner can later use harmless fixtures for ordinary import, duplicate destination, unsupported type and bounded overflow, observing filesystem and database state together. No archive was created or extracted here. This turns a broad Zip Slip alert into an inspectable path, resource and publication argument without treating all three as the same flaw.
Keep import identity stable after extraction
Atlas's path policy protects the staging directory, but the parser must still know which accepted member represents each report. Keep a manifest of canonical member identities and the resulting staged objects. Do not let a later parser reopen files by an original ambiguous name that the extractor normalized differently. The review should follow the same accepted content from member decision to durable record.
For the synthetic duplicate-January case, the whole import is rejected before publication. A preview that shows the first file while a later stage selects the last would undermine the user's understanding even without traversal. Use the accepted manifest for preview and ingestion, or document an equivalent mechanism that binds both to the same content.
Retries also need a fresh private workspace or an explicitly verified clean state. Reusing partially extracted files from a failed attempt can make the next attempt's contents depend on an earlier archive. The import identity, attempt, and publication decision should remain distinguishable. Cleanup failure becomes operational work, not permission to treat leftover files as accepted input.
The owner can review these properties without constructing an exploit archive. A harmless manifest with a duplicate, an unsupported member, and an interrupted attempt supplies concrete expected outcomes. Actual filesystem and database observations would need a separately authorized controlled run. The source conclusion should say whether the reviewed extraction, parser selection, and publication paths preserve the same accepted import, with runtime confinement and resource limits named as separate evidence where unavailable.
Next action
Choose one archive-handling job and answer a single question: which exact operation opens the output file, and what check immediately precedes it? If the concern begins at the upload boundary, review the file-upload lifecycle. For jobs that process hostile files after extraction, continue with background-job review.