Extracting an uploaded ZIP or TAR file can look like a routine file operation: read each archive entry, join its name to an output directory, then write the contents. The security boundary is hidden in that middle step. An archive controls its entry names, and a careless extractor can let those names select files outside the intended destination.
The result can be more serious than a misplaced file. If the process can write to application configuration, startup files, web content, or another user’s data, an archive upload may become an unintended filesystem write primitive.
The defensive goal is precise: every extracted object must remain inside a directory chosen by the application, regardless of the path text stored in the archive. This article develops that rule, shows how path resolution changes the security decision, and covers links, existing filesystem state, resource limits, and verification.
Treat archive entry names as untrusted path input
An archive stores entries with names such as:
images/logo.png
reports/summary.txtThose names describe where entries should appear relative to an extraction root. If the application chooses /srv/import/42 as that root, a normal entry might resolve to:
/srv/import/42/images/logo.pngThe archive, not the application, supplied images/logo.png. That makes the entry name untrusted input.
A dangerous design treats joining strings as authorization:
destination = extraction_root + "/" + entry_name
write(destination, entry_contents)The code has not established that destination still refers to a location under extraction_root. Path syntax can contain parent-directory components, absolute forms, or platform-specific constructs that change where resolution ends.
The threat model is an attacker who can supply or influence an archive processed by an application with filesystem write access. The control aims to stop archive-controlled names from escaping the approved extraction tree. It does not make the archive contents trustworthy, detect malicious documents, or protect locations that the extraction process can reach through other bugs.
Make the decision on the resolved destination
The central mistake is validating the raw entry name while writing to a path derived through different rules.
For example, rejecting names that start with a particular string is fragile because path resolution is structural. The security question is not:
Does this entry name look harmless?It is:
After applying the platform's path rules, is the destination still inside
the extraction root?A simplified defensive flow is:
root = canonical_absolute_path(application_chosen_directory)
for each archive entry:
candidate = resolve(root, entry.name)
if candidate is not inside root:
reject the entry or the entire archive
create the entry using filesystem operations that preserve that boundarycanonical_absolute_path and resolve are conceptual names here, not portable API calls. Real path libraries differ across languages and operating systems. The invariant is portable: normalize the path according to the same semantics that the eventual file operation uses, then enforce containment.
The containment test also needs a directory boundary. A string-prefix test can confuse paths such as /srv/import/42 and /srv/import/420. Use a path-aware relative or ancestry operation supplied by the platform rather than plain text comparison.
This changes the control from a blacklist of suspicious spelling into an allowlist of permitted destinations.
Pick the extraction root before reading entry names
The application should choose the extraction root independently of archive content. A common design creates a dedicated directory for one import job and treats that directory as the only permitted output tree.
That separation gives the security check a stable reference point:
application chooses:
/srv/import/job-8472/
archive may choose:
docs/a.txt
photos/b.jpg
archive must not choose:
anything outside /srv/import/job-8472/Using a dedicated directory also limits accidental collisions with unrelated application files. If practical, run the extraction component with filesystem permissions that allow writes only to its working area. That is defense in depth: path containment handles malicious entry names, while operating-system permissions reduce damage if the containment code has a defect.
A separate working directory can also simplify cleanup. If validation or extraction fails, the application can discard the incomplete job rather than trying to identify every file that may have been created among unrelated data.
Path checks must agree with the actual filesystem operation
Lexical path normalization handles components represented in the entry name, but filesystems add another complication: links and mutable directory entries can redirect later operations.
Suppose the extractor validates a destination under its approved root. If a path component is then resolved through a symbolic link that points elsewhere, the final file operation may reach a different location. Similar problems can appear when an attacker can modify the extraction directory concurrently.
This is the same general security principle used in race-resistant file handling: a check on a pathname is useful only when the later operation cannot silently resolve to a different object.
The exact solution is platform-specific. Depending on the environment, defensive options include:
- refusing symbolic-link and hard-link archive entries unless the application has a defined need for them;
- preventing traversal through symbolic links while creating output paths;
- using directory-relative or handle-based filesystem APIs that keep operations anchored to the approved directory;
- extracting into a directory that untrusted users and unrelated processes cannot modify concurrently.
A simple application that only needs regular files and directories can reject link entries entirely. That removes a class of path-redirection cases and is often easier to review than supporting full archive filesystem semantics.
If link support is required, it needs its own policy. A link target is another path-like value controlled by the archive. Validating only the link’s filename is not enough.
Validate entry types as well as destinations
Archives can represent more than ordinary files. Format and library support varies, but entries may describe directories, links, or other filesystem objects.
An extractor should define the small set of entry types the application actually accepts. For a document-upload workflow, that may be only regular files and directories. Unknown or unsupported types should fail closed rather than being passed through to generic filesystem creation logic.
This is an authorization decision over object type:
entry destination inside approved root
+
entry type allowed by application policy
=
eligible for extractionNeither condition replaces the other. A regular file outside the root is unacceptable, and an unsupported special object inside the root may also be unacceptable.
The policy should match the product’s purpose. A backup restoration tool may legitimately need richer filesystem metadata than an image-import service. Broader format fidelity creates more security cases to specify and test.
Containment does not address resource exhaustion
A path-safe archive can still consume excessive resources. Compressed input may expand to much more data than its upload size suggests. An archive can also contain a very large number of entries, deep directory structures, or files that exhaust storage quotas.
These are availability risks rather than path-containment failures.
Set limits that fit the application’s workload, such as a maximum extracted byte count, maximum entry count, maximum individual file size, and sensible path-depth or name-length constraints. Enforce limits while processing, not only from archive metadata that may be incomplete or untrusted.
Resource limits should be treated separately from destination validation:
path containment -> controls where extraction may write
resource limits -> control how much work and storage extraction may consume
content checks -> control what the application accepts afterwardKeeping these controls conceptually separate makes failures easier to diagnose and policies easier to review.
Decide whether one bad entry rejects one file or the whole archive
When an entry violates the extraction policy, the application needs a predictable failure mode.
Skipping only the offending entry can be appropriate for a tool designed to salvage valid data, but it may produce an incomplete result that downstream code mistakenly treats as complete. Rejecting the whole archive gives transactional behavior that is easier for many upload workflows to reason about.
A robust pattern is to extract into a fresh staging directory, validate the complete result, then make that result available only after every required check succeeds. If processing fails, remove the staging area using the application’s normal cleanup mechanism.
This approach also reduces the chance that another component consumes half-extracted content. It does not by itself provide atomic publication across every filesystem or storage design, so the handoff mechanism still needs to match the application’s consistency requirements.
Test the boundary, not just successful extraction
A test suite should demonstrate that the security invariant survives hostile names and awkward filesystem state.
Start with ordinary nested files and confirm they appear under the selected root. Then add cases that represent boundary conditions: parent-directory components, absolute paths, redundant separators, platform-specific path forms relevant to the deployment environment, link entries if the parser supports them, and collisions with existing files.
The key assertion is stronger than “the extractor returned an error.” After each rejected case, verify that no file was created or changed outside the approved directory.
Also test policy behavior for partial failure. If one invalid entry should reject the whole archive, confirm that the application does not publish files extracted before the failure. If limits are enforced, confirm that exceeding them stops processing and leaves no usable partial result.
Run these tests on every operating-system family the application supports. Path syntax and filesystem behavior are not identical across platforms, so a check proven on one environment should not be assumed to cover another.
Keep the extraction boundary narrow
Archive extraction is safest to reason about when it has a narrow contract: the application chooses an isolated destination, accepts only needed entry types, resolves every output against that destination, and performs writes in a way that cannot escape through links or mutable path components.
That contract still needs neighboring controls. Limit extracted resources, scan or validate content when the application requires it, isolate the extraction process when the impact justifies it, and avoid granting the process write access it does not need.
The practical test is simple to state: for every archive an attacker can supply, can the extractor prove that every filesystem object it creates stays inside the directory the application selected? Build the implementation and tests around that invariant.