Path Traversal: Directory Traversal, LFI & Canonicalization
How ../ sequences escape an intended directory to read arbitrary files, why canonicalization matters, and how allowlists stop path traversal.
An application that serves files from a folder based on a name in the request is making a quiet assumption: that the name refers to a file inside that folder. Filesystems do not enforce that assumption. A filename can point up and out of the intended directory using the parent-directory sequence, and if the application passes the user's name straight to the filesystem, an attacker can walk out of the box and read whatever the process can reach. That is path traversal, also called directory traversal.
It is one of the oldest web vulnerabilities and still one of the most common, because file handling shows up everywhere: downloads, template loading, image serving, log viewers, avatar storage, report generation. Every one of those is a place where a filename crosses from the user into a filesystem call, and every one is a potential traversal bug.
The core mechanism
Suppose an application serves user manuals from a fixed directory and builds the path by appending the requested filename:
/var/www/manuals/ + <user_input>
Ask for intro.pdf and it reads /var/www/manuals/intro.pdf. Ask for this instead:
../../../../etc/passwd
and the path becomes /var/www/manuals/../../../../etc/passwd. Each ../ climbs one directory up. Enough of them reach the filesystem root, and the trailing etc/passwd then descends to a sensitive file far outside the manuals folder. The application meant to expose a directory of PDFs. It exposed the whole filesystem, read-only, to the extent its process account has access.
The .. entry is a real filesystem feature meaning "the parent directory." It exists so that relative paths can move around a tree. Path traversal is just an attacker using that feature against an application that assumed the user would only ever move downward.
The assumption is easy to make and hard to notice. In development, a filename really is just the name of a file in the expected folder, because the developer only ever types well-behaved names. Nothing in the code visibly says "the user may only descend." That rule lives entirely in the developer's head, and the filesystem was never told about it. An attacker who supplies a name the developer never tried is not breaking a rule the code enforces. They are exposing that the code enforced no rule at all.
What the impact looks like
| Outcome | Description |
|---|---|
| Sensitive file read | Configuration files, credentials, source code, private keys, session data |
| Information disclosure | Server details that inform a follow-up attack |
| Local file inclusion (LFI) | The read file is then included or executed by the server, reaching code execution |
| File write / overwrite | In upload flows, a traversal path can place a file outside the intended directory |
Local file inclusion deserves a note. In stacks where the application both reads a file and includes and runs it, a traversal that reaches an attacker-influenced file (an uploaded avatar, a log the attacker can poison) can turn file read into code execution. That is why traversal in a file-inclusion context is more dangerous than traversal in a plain download endpoint, and why it sits close to command injection on the severity scale.
Why naive filtering fails
The obvious defense is to strip ../ from the input. Attackers have several reliable ways around a naive strip:
- URL and double encoding. The sequence can be written percent-encoded, or double-encoded so that one decoding pass leaves a still-encoded
../that a later stage decodes. A filter that ran before the final decode never sees the real sequence. - Absolute paths. If the code appends input to a base but the filesystem treats the input as absolute, an absolute path can ignore the base entirely.
- Nested sequences. Removing
../once from a string like....//leaves../behind, defeating a single-pass replace. - Platform separators. Different operating systems accept different path separators, so a filter tuned for one may miss another.
The pattern is familiar from every injection class: blocklisting the bad thing loses to an attacker who has many spellings of the bad thing. The fix has to work on the resolved meaning of the path, not its surface text.
A related trap is validating too early in a multi-stage pipeline. Input often passes through several layers before it reaches the filesystem: a web framework decodes it once, application code may decode or normalize it again, and the operating system does its own resolution when the file is opened. A check that runs at the first stage inspects a string that no longer matches what the last stage will act on. Each decoding pass can reveal a sequence the previous check could not see. The only stage whose view of the path is authoritative is the one immediately before the file is opened, which is exactly why the containment check belongs there and nowhere earlier.
The mistake that keeps path traversal alive is checking the input before the system resolves it. By the time the operating system opens the file, encodings have been decoded, sequences collapsed, and symlinks followed, and the real target may be nothing like the string you inspected. Always resolve the path to its canonical absolute form first, then check that the canonical result still sits under your allowed base directory. Validate what the filesystem will actually open, not what the user typed.
Canonicalization and the containment check
Canonicalization is the act of reducing a path to its one true absolute form: decoding it, resolving every . and .., and following symbolic links. Once you have the canonical path, you can make a reliable decision, because there is nothing left to reinterpret.
The containment check is then simple to state:
- Take the user input and join it to your fixed base directory.
- Canonicalize the result to an absolute path.
- Confirm that the canonical path still begins with the canonical base directory.
- If it does not, reject the request. Do not read the file.
This works because it operates on the destination the filesystem would actually reach, after all the attacker's encoding tricks have been resolved away. A traversal that escaped the base directory produces a canonical path outside it, and the prefix check catches it.
The stronger pattern: never take a path from the user
Canonicalization plus containment is solid. Removing user-controlled paths altogether is better. Where the design allows it, do not let the user name a file at all. Instead, give each file an identifier and map that identifier to a real filename on the server:
- The user requests document
4821. - The server looks up
4821in a table or a fixed mapping and finds the real path. - The user never supplies a path component, so there is nothing to traverse.
This eliminates the class of bug rather than defending against it, and it composes well with access-control checks, since the lookup is a natural place to confirm the user is allowed that file.
Use an indirection map to keep user input out of the path, and keep the canonicalize-and-contain check as a backstop for any endpoint that genuinely must accept a filename. The map removes most of the surface, and the containment check protects the parts you cannot avoid. Together they mean a traversal payload has nowhere to land even if one layer is misconfigured.
How to defend against path traversal
- Map identifiers to server-side filenames wherever possible, so the user never supplies a path. This is the most complete fix.
- Canonicalize then contain. For endpoints that must accept a name, resolve the full path to its canonical absolute form and verify it still lives under the allowed base directory before opening it.
- Allowlist acceptable input, constraining filenames to a known-safe character set and, where sensible, to expected extensions, instead of blocklisting
../. See our input validation guide for building these checks. - Run with least privilege, so a process that is tricked still cannot read files outside what it legitimately needs. File permissions are a real second line of defense.
- Store uploads outside the web root and serve them through code that applies the checks above, so a traversal in an upload path cannot place executable content where the server will run it.
How bypasses actually look
The list of bypass families earlier is easier to internalize with the shapes attackers reach for. Each one exists because a filter inspects surface text while the filesystem acts on resolved meaning.
- Percent-encoded separators. A
../can be written with its slash percent-encoded, so a filter scanning for the literal three characters never matches. The web layer decodes it later, restoring the real sequence just before the file is opened. - Double encoding. The percent sign itself is encoded, so one decoding pass turns the input into a still-encoded sequence and a second pass turns that into the working
../. A check that runs after only the first decode sees an innocent string. - Nested and overlapping sequences. A payload like
....//survives a single-pass strip of../, because removing the inner sequence leaves the outer characters that recombine into../. Any filter that replaces once rather than resolving fully falls to this. - Absolute path injection. If the code appends user input to a base, but the input names an absolute path, the join can discard the base entirely on some platforms, so no traversal sequence is even needed.
- Mixed separators and trailing tricks. Different platforms accept different separators, and trailing dots or spaces on some filesystems change how a name resolves, so a filter tuned to one convention misses the others.
The common thread is that every one of these looks different on the surface and resolves to the same destination underneath. That is the entire argument for validating the resolved path rather than the raw string. A defense that works on canonical meaning is immune to how many spellings the attacker invents, because all of them collapse to one canonical answer before the containment check runs.
Detection signals
Traversal attempts leave a recognizable trace in request logs and application telemetry. The concrete signals below are worth alerting on.
- Traversal tokens in request parameters. Literal
../and..\\sequences, and their encoded forms, appearing in any parameter that later reaches a file operation. Encoded variants matter as much as the literal ones. - References to sensitive system paths. Requests containing well-known targets such as system password files, application configuration files, or key material, especially aimed at file-serving endpoints.
- File-read errors and anomalous status codes. A burst of not-found or permission-denied responses from a download or file endpoint can indicate an attacker probing for a path that resolves outside the base.
- Access to files the endpoint never legitimately serves. Telemetry that records which real files an endpoint opened, flagging any open outside the intended base directory, catches a successful traversal directly at the filesystem layer.
- Repeated depth escalation. Many requests differing only in the count of parent-directory sequences, climbing from one level to many, is the signature of an attacker feeling for the filesystem root.
The strongest detection watches the resolved file access rather than the request text, for the same reason the strongest defense does: the request text can be disguised, and the file the process actually opened cannot.
How path traversal compares to related bugs
Traversal is one of several ways user input reaches a resource it should not. Placing it beside its neighbors sharpens what is distinct about it.
| Technique | What the attacker controls | Typical result | Core fix |
|---|---|---|---|
| Path traversal | A filename or path that escapes an intended directory | Reading or writing files outside the box | Canonicalize then confirm containment under a base |
| Local file inclusion | A path that the server then includes and executes | Code execution from a reachable file | Never include a path derived from input |
| Server-side request forgery | A URL the server fetches on the attacker's behalf | Access to internal network resources | Allowlist destinations, resolve and validate the target |
| Command injection | A string interpolated into a shell command | Arbitrary command execution | Avoid the shell, pass arguments as data |
Local file inclusion is the closest neighbor and the reason traversal severity varies so widely by context. In a plain download endpoint, a successful traversal discloses files. In a stack that includes or executes the file it resolves, the same traversal reaches code execution, which is why traversal in an inclusion context sits close to command injection on the severity scale. Server-side request forgery rhymes with traversal at a higher layer: both take a locator from the user and reach a resource the user should not be able to name, and both are fixed by resolving the real destination and confirming it stays inside an allowed set.
Common mistakes
Stripping the bad sequence instead of resolving the path. A single-pass removal of ../ loses to nested sequences and encodings. Removal treats the symptom in the surface text while the filesystem acts on the resolved meaning.
Validating too early in the pipeline. A check that runs before the final decode and open inspects a string the last stage will not act on. The only authoritative view of the path is the one immediately before the file is opened.
Trusting the base-directory join. Appending input to a fixed base feels safe, and it fails when the input is an absolute path or contains enough parent sequences to climb out. The join is a starting point for the containment check, never the check itself.
Forgetting the write side. Upload and file-write flows can place content outside the intended directory through the same traversal mechanics. A defense that covers reads while leaving writes unchecked leaves the higher-impact path open.
Relying on file permissions alone. Least privilege is a valuable second line, and it does not stop a traversal from reading anything the process account can already reach, which on a typical server is a great deal. Permissions limit blast radius rather than preventing the bug.
How it evolved
Path traversal is one of the oldest classes of web vulnerability, dating to the earliest days of serving files by name over a network. The basic technique, feeding parent-directory sequences to climb out of an intended folder, has changed very little, because it exploits a legitimate and unchanging filesystem feature. What has evolved is the arms race around filtering. Early defenses tried to strip the offending sequence, attackers answered with encodings and nesting, and defenders eventually converged on the durable answer of resolving the path to its canonical form and checking containment.
Framework maturity shifted the picture as well. Modern web frameworks and file APIs increasingly offer safe path-joining helpers and canonicalization routines that make the correct pattern easier to reach. The bug persists anyway, because file handling is spread across so many features, downloads, template loading, image serving, log viewers, uploads, that a single overlooked endpoint reintroduces the whole class. The defense has been known for a long time. The challenge is applying it uniformly across every place a filename crosses from the user into a filesystem call.
Frequently asked questions
Is path traversal the same as local file inclusion? They overlap. Path traversal is the act of escaping an intended directory to reach another file. Local file inclusion is what happens when the server then includes and executes the file that a traversal reached. Traversal is the mechanism, and inclusion is the more dangerous outcome in stacks that run the resolved file.
Why does stripping the parent-directory sequence fail? Because the attacker has many spellings of the same sequence. Encoded and double-encoded forms, nested patterns that recombine after one removal, and platform-specific separators all bypass a surface strip. The filesystem acts on the resolved path, so a defense has to act there too.
What does canonicalization actually do? It reduces a path to its one true absolute form by decoding it, resolving every current-directory and parent-directory element, and following symbolic links. Once a path is canonical there is nothing left to reinterpret, so a prefix check against the allowed base directory becomes reliable.
Do random or opaque filenames prevent traversal? No. Traversal does not depend on guessing a name, it depends on the application passing a user-influenced path to the filesystem. The fix is to resolve and contain the path, or to remove user-controlled paths entirely by mapping an identifier to a fixed server-side filename.
Where should the containment check run? Immediately before the file is opened, after every decoding and normalization step the input passes through. A check placed earlier inspects a value that later stages will change, so it can be bypassed by a sequence that only appears after a later decode.
How is traversal different from an authorization bug like IDOR? Traversal escapes an intended directory on the filesystem, while an insecure direct object reference fetches an object the user is not entitled to through a missing authorization check. They can appear together when a file endpoint both accepts a path and omits an access check, and each has its own root cause and fix.
Can traversal lead to writing files, not just reading them? Yes. Upload and file-save flows that build a destination path from user input can be steered to place a file outside the intended directory. Writing to a location the server later executes, or overwriting a configuration or startup file, turns a write-side traversal into a path to code execution or persistent compromise. Cover write paths with the same canonicalize-and-contain check you apply to reads, and store uploads outside the web root so a stray file cannot land where the server will run it.
Does running the app as a low-privilege user fix traversal? It limits the damage without removing the bug. A least-privileged process still reads everything its own account can reach, which on a typical server includes application source, configuration, and session data. Treat least privilege as a valuable second line that shrinks blast radius, and keep the resolved-path containment check as the control that actually stops the escape. The two work best together, since one narrows what a successful traversal can reach and the other prevents the traversal from resolving outside the box in the first place.
The delivery side of traversal often involves crafted filenames and file formats, and understanding how file names and formats carry payloads helps you spot the risky endpoints. Our exploit and file-format intelligence lives at the file-formats reference on data.pwnsy.com, which maps how different formats and naming tricks are used in real delivery.
Path traversal endures because file handling is everywhere and the .. sequence is a legitimate filesystem feature that applications forget to account for. The defense is not to chase every spelling of the escape. It is to decide the real destination the filesystem will reach, confirm it stays inside the box you meant, and, better yet, stop handing the filesystem raw user paths in the first place.
Related guides
Sources & further reading
- OWASP: Path Traversal — OWASP
- PortSwigger Web Security Academy: Directory Traversal — PortSwigger
- OWASP File Upload / Path Handling Cheat Sheet — OWASP
Related guides
- Clickjacking Explained: UI Redress Attacks & Frame Defenses
How clickjacking hijacks a user's clicks with transparent iframes, what an attacker can trigger, and how to block framing with headers and CSP.
- Command Injection: Shell Metacharacters & Safe APIs
How OS command injection breaks out of an intended command via shell metacharacters, and why argument-array APIs beat string concatenation.
- CSRF Explained: SameSite Cookies & Anti-CSRF Tokens
How cross-site request forgery rides a logged-in user's cookies to force state-changing requests, and how SameSite, tokens, and double-submit stop it.