Extracting an archive looks like a simple file operation: read each entry, join its name to an output directory, and write the result. The dangerous part is that an archive controls those entry names. If extraction code treats them as trusted relative paths, a crafted entry can make a write escape the directory chosen by the application.
That failure is commonly called archive path traversal. It can turn an upload, package import, backup restore, or document-processing feature into an unintended filesystem write. The consequence depends on the extractor’s permissions: files outside the extraction area may be created or replaced, including files later consumed by other parts of the system.
The defensive rule is narrow and reusable: an archive entry may choose a location only inside the extraction root. This article explains how to enforce that rule, why simple string checks are unreliable, where symbolic links complicate the model, and how to test the boundary without depending on a particular archive library.
An archive entry name is untrusted path input
An archive format stores metadata for each entry. For a file, that metadata normally includes a name such as:
images/logo.pngAn extractor might combine that name with a destination directory:
extraction root: /srv/imports/job-42
entry name: images/logo.png
result: /srv/imports/job-42/images/logo.pngThat is the intended case. The mistake is assuming every entry name has the same shape.
A name can contain path syntax with special meaning to the filesystem or path library. Parent-directory components are the clearest example. Absolute paths are another. Platform-specific separators, drive or volume syntax, and unusual normalization behavior can matter when software accepts archives produced on different systems.
The security decision therefore cannot be “the archive parser accepted this name.” Parsing establishes what the archive says. It does not establish where your application should allow that entry to be written.
State the threat model before choosing the boundary
Assume an untrusted or less-trusted party can supply an archive that a trusted application extracts. The application chooses an extraction root and has filesystem permissions beyond that directory.
The control in this article reduces the risk that archive-controlled names cause writes outside the chosen extraction root. The boundary applies to every extracted filesystem object, not only ordinary files.
It does not make the contents of an extracted file trustworthy. A parser can still have vulnerabilities when it later opens that file. It does not limit decompression resource use, remove malware, or correct excessive permissions held by the extraction process. Those are separate controls.
There is also a stronger attacker model to consider: another process or user may be able to modify the extraction directory while extraction is running. In that case, validating path text before a later write can be insufficient because filesystem state can change between validation and use. A higher-risk extractor needs filesystem operations that preserve confinement while resolving and creating objects, not only a pre-write pathname check.
Resolve the destination before writing
The smallest useful design has four steps:
1. choose a trusted extraction root
2. read an entry name from the archive
3. resolve the candidate destination under that root
4. write only if the resolved destination remains inside the rootThe important word is resolve. Security code should use the platform’s path semantics rather than searching the raw entry string for suspicious text.
Conceptually:
root = normalized_absolute_path(trusted_destination)
candidate = resolve_under(root, archive_entry_name)
if not is_descendant(candidate, root):
reject_entry()
else:
extract_to(candidate)This is deliberately pseudocode. Real path APIs differ across languages and operating systems, especially in how they handle absolute components, separators, symbolic links, nonexistent paths, and case sensitivity.
The invariant is portable even when the API is not: after interpreting the entry name using the same path rules that the write will use, the destination must still be a descendant of the trusted extraction root.
Why prefix checks are easy to get wrong
A tempting implementation converts both paths to strings and checks whether the candidate starts with the root:
root: /srv/imports/job-42
candidate: /srv/imports/job-420/other.txtThe candidate string begins with the root string, but it is not inside the root directory. The comparison confused a textual prefix with a path-component relationship.
Appending a separator before a prefix comparison fixes this particular example but still leaves platform and normalization details for application code to reproduce. A path-aware descendant check is easier to reason about when the standard library provides one.
The same principle explains why rejecting names containing .. is not a complete design. A raw substring test can reject harmless names that merely contain two dots while missing other ways a path API can interpret an entry as rooted or outside the intended namespace. Validate the destination property you actually need rather than maintaining a growing list of suspicious spellings.
Treat absolute entry names as outside the contract
An extraction feature usually has no reason to honor an absolute path stored in an untrusted archive. The application, not the archive, should choose the output root.
If a path-combination API allows an absolute child path to replace or ignore the base path, blindly joining the two values can discard the intended boundary. Extraction code should reject rooted entry names or use an API whose semantics explicitly require a relative path under the selected root.
This is also where cross-platform behavior deserves attention. A service that extracts only on one operating system should validate according to that system’s actual path rules. Software that processes archives across several platforms needs tests for each supported platform rather than assuming path syntax is universal.
Directories and files need the same confinement rule
Checking only regular-file entries leaves gaps in the model. Directory entries influence where later files are created, and link-like entries can affect how later paths resolve.
Apply the confinement decision before creating any filesystem object derived from archive metadata. Do not create a directory first and decide whether its path was acceptable afterward.
It also helps to separate two questions:
Is this entry type allowed by the feature?
Is this entry destination confined to the extraction root?A simple import feature may need only regular files and directories. If it has no reason to reproduce symbolic links, hard links, device-like entries, or other special objects, rejecting those types keeps the trust boundary smaller. Supporting more entry types is a feature decision with additional filesystem semantics to defend.
Symbolic links can invalidate a pathname-only check
Suppose an extractor first creates a symbolic link inside the extraction directory and later writes a file through a path that passes through that link. The later pathname may look as though it is under the root while filesystem resolution sends the write elsewhere.
This is why “normalize the string and check the prefix” is not a complete answer for an extractor that supports links.
The simplest defensive choice is often to reject symbolic-link entries when the application does not need them. That removes archive-created symlinks from the extraction model.
If links are required, the implementation needs stronger guarantees. It must reason about the actual filesystem objects traversed during creation and opening, including links that already exist. The exact safe APIs are operating-system and language dependent. Prefer mechanisms that let the process resolve or create descendants relative to a trusted directory handle while constraining link traversal, when the target platform provides them.
There is a related operational rule: extract into a directory that untrusted actors cannot modify concurrently. Otherwise another actor may replace a checked path component between the application’s validation and its later use. Path confinement and race-resistant filesystem access solve different parts of the same trust-boundary problem.
Decide what to do when one entry is invalid
Rejecting an unsafe entry is necessary, but extraction behavior after rejection also matters.
For security-sensitive imports, failing the entire extraction is usually easier to reason about than silently skipping an entry and reporting success. A partially extracted tree may violate application assumptions even when every file that was written stayed inside the root.
A useful pattern is to extract into a fresh staging directory, validate and process the complete result, then make that result available only after the operation succeeds. If extraction fails, discard the staging result. This reduces the chance that another component consumes a half-finished import.
Whether all-or-nothing publication is necessary depends on the feature. A user-facing tool that intentionally recovers valid files from damaged archives may choose different behavior, but that should be an explicit product decision rather than an accidental side effect of error handling.
Keep extraction privilege narrow
Path validation is strongest when a mistake does not grant broad filesystem authority.
Run extraction with only the permissions it needs. A process whose writable area is limited to a dedicated working directory has a smaller failure impact than one that can modify application code, configuration, shared data, or other users’ files.
This is defense in depth, not a substitute for confinement. Permission boundaries can be misconfigured, and an escaped write may still damage something valuable within the process’s writable scope. The application should enforce both: the entry stays under the selected root, and the extractor itself has limited authority.
For especially sensitive workflows, isolating extraction in a dedicated worker or sandbox can further reduce impact from archive parsers and file-processing code. That extra complexity is justified when archives are highly untrusted or subsequent processing uses complex native parsers. A small internal tool processing archives from a trusted build system may reasonably use a simpler design if its trust assumptions are documented and enforced.
Test the invariant, not just known bad strings
Tests should demonstrate that no accepted entry can cause a write outside the extraction root.
Start with normal nested paths and verify that they extract where expected. Then test categories that challenge the boundary: parent-directory components, rooted paths recognized by the target platform, empty or unusual names your library permits, and link entries if the format and implementation support them.
The most valuable assertion is about the filesystem result. After extraction, every object created by the operation should be inside the staging root, and a deliberately invalid archive should not create or replace an object outside it.
Also test failure behavior. If one entry is rejected, confirm whether the entire import is discarded or whether partial output remains according to the application’s documented policy. If the extraction library has convenience APIs that unpack a whole archive automatically, verify their documented path-safety behavior rather than assuming they enforce the boundary you need.
Common mistakes come from validating the wrong representation
Several weak designs share the same underlying problem: they make a security decision about something other than the destination the filesystem will use.
Checking a filename extension says nothing about its path. Searching for .. reasons about characters rather than resolved components. Normalizing a path but never checking its relationship to the root produces a cleaner unsafe path. Checking only file entries ignores directories and links. Validating once and then allowing an untrusted actor to change the directory tree creates a race between the decision and the write.
A better review question is: for every filesystem object this archive can cause us to create, what proves that the actual destination remains under the extraction root at the moment we create or open it?
That question scales from a small application using a safe library helper to a high-assurance extractor using directory-relative operating-system primitives.
Make the extraction root a real security boundary
Safe archive extraction is not mainly about recognizing a list of malicious filenames. It is about preserving one invariant while untrusted metadata is translated into filesystem operations.
Choose the extraction root from trusted application state. Treat every archive entry name and entry type as untrusted. Resolve destinations using the target platform’s path semantics, reject anything that does not remain under the root, and avoid link behavior you do not need. Where concurrent filesystem modification is possible, use stronger directory-relative or handle-based operations so a pathname check cannot become stale before the write.
Then verify the boundary with filesystem-level tests and narrow the extractor’s permissions. The practical next step is to inspect every place your application unpacks user-controlled or externally supplied archives and identify exactly where that confinement decision is enforced.