Extracting an archive looks like a file-copying task: read each entry, join its name to a destination directory, and write the bytes. The security problem is that an archive entry name is input chosen by whoever created the archive. If that name can influence the output path without a containment check, extraction can write outside the directory the application intended to grant.
The consequence is broader than a misplaced file. Depending on the process’s permissions, an escaped write may replace application data, configuration, generated assets, or another file that the extractor can modify.
The useful mental model is: an archive grants names for files inside a destination; it does not grant authority to choose arbitrary filesystem locations. This article explains how to preserve that boundary, why simple string checks are unreliable, how links and existing filesystem state complicate the problem, and how to test the extractor defensively.
The archive name is data, not a destination path
Suppose a service accepts a project bundle and extracts it under a fresh working directory:
/work/jobs/7421/An ordinary archive might contain entries such as:
README.md
assets/logo.svg
docs/setup.txtThose names describe locations within the extraction root. The application can safely support that useful behavior without allowing the archive to choose locations above or beside /work/jobs/7421/.
The mistake is to treat the entry name as if it already were an authorized filesystem path:
output = destination + entry_name
write entry to outputAn entry name can contain path syntax that changes what the resulting path means. Parent-directory components are the familiar example, but the broader problem is not one particular spelling. Absolute paths, platform-specific path forms, links, and filesystem behavior can all affect where a write eventually lands.
So the defensive question is not, “Does this name look harmless?” It is, “Will this extraction operation create or modify an object inside the directory I authorized?”
Separate naming from authority
A robust design gives the application, not the archive, control over the extraction root.
For each entry, the extractor should conceptually perform these steps:
trusted extraction root
+
untrusted entry name
|
v
resolve candidate location
|
v
candidate is inside root? ---- no ---> reject
|
yes
v
create output under controlled rulesThis model separates two concerns.
The archive supplies a relative name. That is useful data because directory structure is part of many archive formats.
The application supplies the authority boundary. The entry may name an object only within that boundary.
Keeping those roles separate makes the security property easier to reason about: no accepted entry should cause extraction to modify a filesystem object outside the intended root.
Why substring checks do not establish containment
It is tempting to reject names containing a suspicious substring such as ... That is not a sound containment rule.
First, the same characters can appear in an ordinary filename. Rejecting every occurrence can block legitimate input without proving anything about the final location.
Second, path interpretation is platform-dependent. Separators, absolute-path forms, case behavior, prefixes, and normalization rules are not identical across filesystems and operating systems. A filter written around one textual representation can disagree with the path API that later opens the file.
Third, the security property concerns the resolved location, not the raw spelling. Validation should therefore use the same path semantics as the operation that follows it.
A better pattern is:
- require an archive entry to represent an allowed relative path;
- resolve that relative path beneath the trusted extraction root using the platform’s path facilities;
- verify that the resulting location remains contained by the root;
- only then create the output using a method that preserves that assumption.
The exact APIs differ by language and operating system. The principle does not.
Containment is a path relationship, not a text prefix
Even after normalization, a plain string-prefix comparison can be wrong.
Consider these two directories:
/work/jobs/7421
/work/jobs/74210The second textual path begins with the first string, but it is not a child of the first directory. A correct containment check must understand path components or use a relative-path operation that can determine whether the candidate escapes the root.
A useful abstract test is:
relative = path_relative_to(root, candidate)
if relative is absolute:
reject
if relative escapes root:
reject
acceptThis is pseudocode, not a portable API. In production, use the path library provided by the implementation platform and define what its relative-path result means before relying on it.
The important point is that containment is structural. /work/jobs/7421/report.txt is below the root because its path components descend from that directory, not because a particular byte string happens to share a prefix.
Validate before creating filesystem objects
The order of operations matters.
If the extractor creates directories or files first and checks the path afterward, the security-sensitive effect has already happened. Validation must occur before the write that depends on it.
A simple extraction loop therefore needs a clear boundary:
for each archive entry:
validate entry type
derive contained destination
reject if destination is outside root
create required directories under controlled rules
create file without unintended overwrite behavior
copy bounded entry dataThe entry-type check belongs early because archives can represent more than ordinary files and directories. Depending on the format, an entry may describe a symbolic link, hard link, device-like object, or metadata with effects that the application does not need.
If the product only needs ordinary files and directories, the simplest defensible policy is usually to accept only those types. Supporting additional entry types expands the security model and should be an explicit product requirement, not an accidental consequence of a general-purpose extraction library.
Links can invalidate a path-only assumption
A lexical path can appear to stay inside the root while filesystem links redirect later traversal elsewhere.
Imagine that the extraction root contains a directory entry that is actually a symbolic link to a location outside the root. A later output path that descends through that link may be lexically contained but resolve to an external location when opened.
This is why safe archive extraction is not only a normalization problem.
There are two practical cases to distinguish.
A private, freshly created extraction tree
If the application creates a new extraction directory, does not allow untrusted parties to modify it concurrently, rejects archive link entries, and controls every directory created during extraction, the filesystem state is much easier to reason about. Under those assumptions, component-aware containment checks plus controlled creation provide a strong and understandable design.
This is often the preferable architecture for server-side processing: extract into a dedicated temporary or job directory that is not shared with unrelated writers.
A shared or attacker-modifiable tree
If another actor can replace path components while extraction is running, checking a path and opening it later creates a time-of-check/time-of-use problem. The filesystem may change between those operations.
In that environment, a precomputed path string alone is not enough. The implementation needs platform-specific filesystem primitives that constrain traversal relative to an already trusted directory and avoid following unwanted links during component resolution. The available guarantees differ across operating systems and runtimes.
The design decision is important: when possible, remove the race by extracting into a private tree rather than trying to make a shared writable tree behave like a stable trust boundary.
Decide what overwrite means before extraction
Containment answers where an entry may write. It does not answer what existing content it may replace.
For a fresh extraction directory, rejecting pre-existing output files is simple and reduces ambiguity. It also helps detect duplicate or conflicting archive entries instead of silently letting archive order decide which content wins.
If extraction intentionally updates an existing tree, overwrite policy becomes a separate security decision. Ask which existing files the operation is authorized to replace, whether links or special objects may already exist at those names, and what happens if the extraction fails halfway through.
Do not let a library’s default overwrite behavior make that decision implicitly.
For sensitive workflows, extracting into a fresh staging directory and promoting the result only after validation can provide a cleaner failure model. Promotion itself must still follow the application’s authorization and atomicity requirements; staging does not make the extracted content trustworthy.
Bound resource use as a complementary control
Path containment prevents escaped writes. It does not protect the service from an archive that consumes excessive storage, memory, CPU time, file descriptors, or numbers of filesystem objects.
An extractor that processes untrusted archives should therefore enforce resource limits appropriate to the product. Useful limits can include:
- maximum archive size before extraction;
- maximum total uncompressed bytes;
- maximum size for an individual entry;
- maximum number of entries;
- maximum path depth or name length where the platform requires it;
- bounded processing time or job resources.
These controls address a different threat. A perfectly contained archive can still exhaust a worker if expansion is unbounded.
Check limits while data is processed, not only metadata declared by the archive. Metadata can be useful for early rejection, but the implementation should not assume untrusted size declarations are a complete enforcement mechanism.
Keep permissions narrower than the bug’s consequences
The extraction process should have only the filesystem access it needs.
If a worker needs to write only to a job directory, giving it write access to application code, shared configuration, or unrelated user data increases the impact of any missed path-handling flaw. Least privilege does not repair incorrect containment, but it can reduce the damage if containment or another control fails.
Isolation can strengthen this boundary further. For higher-risk workloads, a dedicated worker, container, sandbox, or other platform isolation may be justified when it meaningfully limits reachable files and resources. The exact mechanism depends on the deployment platform.
Do not confuse an application-level destination check with operating-system isolation. They are complementary controls with different failure modes.
Test the security property, not just ordinary archives
A useful test suite should demonstrate that valid nested files work and that boundary-crossing attempts do not create external effects.
Start with a temporary directory whose layout the test controls. Put a sentinel file outside the extraction root and record its contents. Then exercise the extractor with cases that cover the path semantics and entry types your implementation supports.
The assertions should focus on outcomes:
accepted entries appear only below extraction_root
rejected entries create no outside files
outside sentinel remains unchanged
unsupported entry types are rejected
resource limits stop oversized workAlso test platform-specific path forms on every operating system the application supports. A test that passes on one path implementation does not prove equivalent behavior on another.
If the extractor intentionally supports links, tests need to cover their resolution rules and interaction with later entries. That is a more complex security contract than rejecting links, so support it only when the product needs it.
Understand what containment does not solve
A contained file can still be dangerous to process.
The extracted bytes may be malformed, unexpectedly large, or crafted to exercise bugs in parsers that consume them later. A contained HTML or image file may also need careful serving behavior if users can retrieve it through a web application. Authorization is still required before one user can access another user’s extracted content.
Containment also does not establish that an archive came from a trusted publisher. If provenance matters, use an appropriate authenticity mechanism separately.
The threat model for this control is narrower: it reduces the risk that attacker-controlled archive names or entry types turn an extraction operation into filesystem writes outside the intended destination. It does not make archive contents trustworthy and it does not replace resource limits, least privilege, content validation, authentication, or authorization.
Choose the simplest extraction contract that meets the product need
Many applications do not need every feature an archive format can express. A service that accepts document bundles may need only regular files and directories beneath one fresh destination.
That narrow contract is easier to defend:
relative regular-file names only
+ controlled directories
+ component-aware containment
+ no archive links or special entries
+ no unexpected overwrite
+ bounded extractionA richer contract can be valid, but each additional capability needs explicit semantics and tests. If symbolic links are required, define where they may point and how later entries interact with them. If existing files may be replaced, define which ones. If permissions or executable bits are preserved, decide whether that metadata is appropriate in the destination environment.
Security improves when the extractor implements the capability the product actually needs rather than the maximum capability the archive format can encode.
Conclusion
Archive extraction is a filesystem authorization problem disguised as a convenience operation. The archive may choose names within a directory, but the application must keep control of the directory boundary.
Treat every entry name and type as untrusted. Resolve names with the platform’s path semantics, verify structural containment before writing, reject capabilities such as links when they are unnecessary, and prefer a fresh private extraction tree where filesystem state cannot change underneath the check. Then add resource limits and least privilege for the risks that containment does not address.
The practical test is simple to state: after processing any accepted archive, every filesystem effect caused by extraction should remain within the destination and within the capabilities the application deliberately granted.