An application that accepts ZIP, TAR, or similar archives may appear to be handling one uploaded file. During extraction, however, the archive can ask the application to create many filesystem objects with names chosen by whoever created the archive. If those names are treated as trusted paths, extraction can write outside the directory the application intended to use.
The consequence can be more serious than a misplaced file. Depending on the process permissions and surrounding system, an unintended write might replace application data, alter configuration, or place content where another component will later consume it.
The defensive idea is to treat every archive entry name as untrusted path input. Before creating anything, derive the destination under a dedicated extraction root and verify that the resulting operation cannot escape that boundary. This article explains that mental model, why simple string checks are insufficient, how links complicate the problem, and what to verify in a production extraction path.
An archive contains names, not trusted destinations
Suppose a service receives an archive and intends to unpack it under:
/work/import-4821/A normal archive might contain entries such as:
report.pdf
images/chart.png
notes/readme.txtIt is tempting to implement extraction conceptually as:
destination = extraction_root + entry_name
write entry to destinationThat model gives the archive too much authority. An entry name can contain path syntax whose meaning depends on the filesystem and platform. If the extraction code accepts an absolute path or path components that move to a parent directory, the final destination may no longer be under /work/import-4821/.
The safer mental model is:
archive entry name = a request to create an object
extraction policy = the authority to decide where that object may existThe archive proposes a relative name. The extractor decides whether that name can be represented safely inside its assigned root.
This is a path-confinement problem. The trust boundary is not the archive file itself; it is the point where archive-controlled metadata becomes a filesystem operation.
State the threat model
Path confinement reduces the risk that a crafted or malformed archive causes extraction to create or replace files outside its assigned directory. The same control also protects against accidental archives containing unexpected absolute or parent-relative names.
The attacker needs a way to influence archive contents and a service that extracts those contents with useful filesystem permissions. They do not need permission to choose the final destination directly if the extractor derives that destination unsafely from archive metadata.
Confinement does not make arbitrary archive contents trustworthy. A file that stays inside the extraction directory can still contain malicious documents, invalid data, or content that exploits a later parser. Confinement also does not by itself control decompression size, file count, CPU consumption, or storage exhaustion. Those are separate resource risks and need explicit limits.
Finally, the control is only as strong as the permissions of the extraction process and the integrity of the extraction root. If another actor can rearrange directories or links while extraction is running, a check performed earlier may no longer describe the filesystem state at the moment of the write.
Validate the destination, not a suspicious substring
A weak defense looks for a particular spelling such as ../ and rejects entries containing it. That is easy to understand, but it answers the wrong question.
The security question is not:
Does this name contain a substring I dislike?It is:
Will this filesystem operation remain inside the extraction root?Those questions differ because path interpretation is platform-dependent. Absolute path forms, separators, repeated components, and normalization rules are not identical everywhere. A filter designed around one textual pattern can miss another representation with the same dangerous meaning.
A stronger design starts by rejecting path forms that the archive format or application does not need, such as absolute destinations. It then interprets the remaining entry as a relative path under the extraction root, normalizes it using the platform’s path semantics, and verifies that the candidate destination is still a descendant of that root.
Conceptually:
trusted root: /work/import-4821
entry name: images/chart.png
candidate: /work/import-4821/images/chart.png
containment check: candidate is inside trusted root -> acceptFor an entry that would resolve outside the root, extraction rejects the entry before creating the object.
The important detail is that containment is a path relationship, not a string-prefix relationship. A textual prefix test can confuse neighboring names such as /work/import-4821-old with descendants of /work/import-4821. Use path-aware APIs that can determine ancestry according to the target platform.
Decide what entry types the application actually needs
Archives can represent more than ordinary files and directories. Depending on the format and library, entries may describe symbolic links, hard links, device-like objects, permissions, ownership metadata, or other filesystem properties.
A general-purpose archiver may need to preserve much of that information. An application import feature often does not.
That difference should drive policy. If an upload feature only needs documents and directories, accepting link entries creates complexity without providing user value. Rejecting unsupported entry types is usually easier to reason about than trying to preserve every feature of the archive format safely.
This is an example of reducing authority: the archive should be able to express only the filesystem objects the application intends to accept.
Links can move the effective destination
Path validation becomes more subtle when links are involved.
Imagine that an extractor first creates a directory entry that is actually a symbolic link. A later archive entry appears textually to be under that directory. If normal filesystem path resolution follows the link, the later write can land somewhere else.
The lesson is broader than any one archive format:
lexical path containment is not enough if filesystem objects can redirect resolutionThere are several defensible strategies, and the right one depends on the application and platform.
For simple upload and import workflows, the clearest strategy is often to reject symbolic links and hard links entirely. Then create only regular files and directories beneath a fresh extraction root that untrusted users cannot modify concurrently.
If an application genuinely must preserve links, it needs a stronger design. Link targets need their own validation, and filesystem operations should be performed in a way that resists link-following and race conditions. The exact APIs for doing this safely are operating-system-specific, so a portable application should use a well-maintained extraction library that documents these guarantees rather than rebuilding them from string operations.
A check and a write must describe the same filesystem state
A common secure-coding pattern is:
1. compute destination
2. verify destination is inside root
3. open destination
4. write dataThat is useful, but there is an important boundary condition. If an attacker can change a directory component between steps 2 and 3, the path checked may not lead to the same object when it is opened. This is a form of time-of-check/time-of-use race.
For many application import jobs, a practical way to reduce this risk is operational rather than clever: extract into a newly created directory owned by the extraction process, do not expose that directory to untrusted writers during extraction, and reject links. Under those assumptions, path components are not expected to be replaced by another actor while the job runs.
Higher-risk systems may need directory-relative or handle-based filesystem APIs that bind operations to already-open directories and refuse unwanted link traversal. Those mechanisms vary by operating system and language runtime. The general principle remains the same: do not rely on a path validation step if an untrusted actor can change what that path resolves to before the write occurs.
Treat library convenience functions as security-sensitive APIs
Many archive libraries provide a one-call “extract all” operation. Whether that operation is appropriate depends on the library, version, archive format, platform, and the guarantees documented by that API.
Do not assume that a familiar convenience function enforces your application’s policy. Some libraries sanitize dangerous names, some reject them, some expose safer modes, and behavior can change across versions. Even a library that confines paths may still preserve entry types or metadata your application does not want.
Before using an extraction API for untrusted archives, answer four questions from its current documentation and your own tests:
- How does it handle absolute and parent-relative entry names?
- Can entries create symbolic links or hard links, and can later entries traverse them?
- Which permissions, ownership information, and special entry types can it restore?
- What limits can you apply to extracted bytes, entry count, and other resource consumption?
If the API’s guarantees are unclear, wrap it with explicit validation or choose an API whose security behavior is documented. Avoid copying a path-sanitization snippet from another platform and assuming its rules transfer unchanged.
Keep extraction authority small
Path validation reduces where an archive can write. Process permissions determine how damaging a mistake can become if validation fails.
An extraction worker usually does not need write access to application binaries, deployment configuration, credentials, or unrelated user data. Give it a dedicated working directory and only the permissions required for the import job. This is least privilege applied to filesystem writes.
A useful flow is:
untrusted archive
|
v
isolated extraction root
|
v
validate names and entry types
|
v
extract with resource limits
|
v
validate resulting content
|
v
move approved data into its final locationThe final move is a separate trust decision. Keeping extraction in a staging area means the archive does not write directly into a live application directory merely because its paths passed a containment check.
For low-risk workflows, a dedicated private temporary directory, ordinary-file-only policy, path confinement, and reasonable resource limits may be sufficient. For archives processed automatically with elevated privileges or whose output feeds sensitive systems, isolation and stronger filesystem controls provide useful defense in depth.
Verify the boundary with adversarial tests
A security control is easier to trust when tests exercise the boundary it claims to enforce.
Build test archives containing normal nested files and confirm that they extract successfully. Then include entries representing the path forms your application rejects: parent-relative names, absolute names, unsupported link types, and platform-specific edge cases relevant to the systems you deploy on. The expected result should be rejection without creating an object outside the extraction root.
Also test mixed archives where an invalid entry appears after valid ones. Decide whether the entire extraction should fail atomically or whether partial output is acceptable. If partial output is not useful, extracting into a disposable staging directory makes cleanup simpler: on any validation or extraction failure, discard the staging directory rather than trying to reason about which individual files are trustworthy.
Resource tests matter too. Path confinement and resource budgets protect different properties. Confirm that limits on expanded bytes, entry count, individual file size, and other relevant costs stop work before the extraction host exhausts its resources.
Common designs that leave gaps
Checking only for ../ leaves the policy tied to one textual representation rather than the destination the filesystem will use.
Checking only the final string prefix can confuse sibling paths with descendants and may ignore platform-specific path semantics.
Validating names while allowing archive-created links can let later filesystem resolution undermine an otherwise reasonable lexical check.
Extracting directly into a live application directory increases the consequence of any validation mistake and makes recovery from partial failure harder.
Running the extractor with broad write permissions turns a path-handling bug into a larger authority problem.
Finally, assuming that path confinement also handles decompression bombs or malicious file contents mixes separate threat models. A strong extraction design composes controls: confinement for destination authority, resource budgets for availability, least privilege for blast radius, and content validation for whatever consumes the extracted files next.
Conclusion
Safe archive extraction is primarily an authority problem. An archive may supply names and content, but it should not decide where on the host filesystem those objects are allowed to exist.
Treat every entry name as untrusted path input. Resolve it under a dedicated extraction root with path-aware containment checks, reject entry types the application does not need, and account explicitly for links and concurrent filesystem changes. Keep the extraction process narrowly privileged and stage output away from live application files.
The practical test is simple: for every object an archive can cause the application to create, you should be able to explain why that object must remain inside the intended root even when the archive controls its metadata. If that explanation depends only on a substring check or on trusting the archive library by default, the boundary needs a stronger design.