Application & code
Educational11 minPath Traversal
An application builds a filesystem path from untrusted input without restricting it to an intended directory, so traversal sequences let the attacker read or write files outside that directory — configuration, secrets, source code, or arbitrary locations on the host.
Classification
- Domain
- Application & code
- ROOTVIEL support
- Educational — Recognized concept, documented for learning — not detected by ROOTVIEL.
- Analysis family
- code-src
- CWE
- CWE-22
- OWASP
- A01:2021-Broken Access Control
Definition
Path traversal (directory traversal) is the failure to confine a file operation to the directory the application intended. The code takes untrusted input — a filename, a path segment, a download or template identifier — and joins it onto a base directory to form a path, then opens, reads, writes, or serves that path. If the input can climb out of the base directory (classically with "../" sequences, absolute paths, or encoded variants), the file operation reaches files the user was never meant to touch.
It is fundamentally an access-control failure expressed through the filesystem: the intended policy is "only files under this directory", and the defect is that the resolved path is never checked against that boundary. The same logic applies to reads (leaking secrets) and writes (overwriting files, which can become code execution if a writable path is later executed).
Context
Path traversal appears wherever user input influences which file is accessed: file download and preview endpoints, static-asset or avatar serving, template and include resolution, archive extraction (zip-slip, where an entry name contains traversal), log viewers, and import/export features. It also appears in reverse: writing an uploaded file to a path derived from its attacker-chosen name.
- ▸Download/serve endpoints: GET a file whose name or id comes from the request.
- ▸Template, locale, or plugin loaders that resolve a file from a user-supplied identifier.
- ▸Archive extraction where an entry’s path contains "../" (zip-slip), landing files outside the extraction directory.
- ▸Upload handlers that build the destination path from the client-supplied filename.
- ▸Second-order cases: a stored path is read back later and used in a file operation without re-validation.
Technical fundamentals
The key operation is path resolution. A filesystem interprets "." and ".." relative to the current segment, so "base/../../etc/hosts" resolves upward out of "base". Simply concatenating a base directory and user input does not constrain anything, because the result is only a string until the OS resolves it. The only robust check is to canonicalise the full path (resolve symlinks and "." / ".." to an absolute real path) and then verify that the canonical result is still inside the canonical base directory.
This is a taint dataflow where the sink is a filesystem call (open, read, write, serve, extract). A vulnerability exists when tainted input reaches that sink as part of the path with no canonicalise-then-contain check. Naive defences fail for predictable reasons: stripping "../" once can be bypassed with "....//" (which collapses back to "../" after one removal) or with URL/Unicode/overlong encodings; and checking the input string before resolution misses symlinks and OS-specific separators.
- Canonicalisation
- Resolving a path to its absolute, symlink-free real form so that "." / ".." and links are already applied before any check.
- Containment check
- Verifying the canonical path is a descendant of the canonical base directory — the control that actually enforces the boundary.
- Zip-slip
- Path traversal via an archive entry whose stored name contains "../", so extraction writes outside the target directory.
- Encoded traversal
- Traversal hidden behind URL-encoding, double-encoding, Unicode, or "....//" tricks that defeat single-pass string filtering.
Root cause
The root cause is building a path from untrusted input and never checking the resolved path against the intended boundary. The developer assumes that joining a base directory to a filename keeps the result inside that directory, but join is pure string work — it does not enforce containment, and ".." in the input climbs out. The fix is to resolve first, then confirm the real path is inside the real base; validation of the raw string alone is not the same thing.
A contributing cause is treating traversal as a character-filtering problem ("remove ../"). Because encodings, double-encoding, and collapse tricks defeat single-pass filters, the durable control is canonicalise-then-contain, ideally combined with never using the raw filename at all — map the request to an identifier and let the server own the actual path.
How the attack works, step by step
- 1A download endpoint joins a base directory to a user-supplied filename and serves the result.
- 2The attacker supplies a filename containing traversal sequences, e.g. "../../../../etc/hostname" (or an encoded form, or an absolute path on some platforms).
- 3The server joins and the OS resolves the path, climbing out of the base directory to the targeted file.
- 4The file is read and returned, leaking secrets, configuration, source code, or credentials — or, in a write context, an uploaded file lands at an attacker-chosen location.
- 5Because the resolved path was never checked against the base directory, nothing stopped the climb.
A write-side escalation: if the attacker can place a file into a directory that is later executed or auto-loaded (a scripts folder, a plugin path), traversal becomes remote code execution. Zip-slip achieves the same by hiding "../" in archive entry names.
Vulnerable example
A synthetic, illustrative download handler that joins a base directory to the request filename and serves it, with no containment check:
synthetic — do not use; illustrates the flawconst path = require("node:path") const fs = require("node:fs") const BASE = "/var/app/user-files" // UNSAFE: the resolved path is never checked against BASE. function download(req, res) { const name = req.query.file const full = path.join(BASE, name) // join does NOT prevent climbing out // "../" in name resolves above BASE before the file is read. const data = fs.readFileSync(full) res.send(data) } // Attacker sends file: ../../../../etc/passwd // Resolved path escapes BASE and the file is returned.
Corrected example
The fix is canonicalise-then-contain: resolve the full path to its real, absolute form and confirm it is still inside the base directory before any file operation. Better still, do not use the raw filename at all — map a request id to a server-owned path.
synthetic — resolve, then verify containmentconst path = require("node:path") const fs = require("node:fs") const BASE = fs.realpathSync("/var/app/user-files") // canonical base // SAFER: resolve the candidate and confirm it is inside BASE. function download(req, res) { const candidate = path.resolve(BASE, req.query.file) // Containment check on the resolved path (not the raw string). const rel = path.relative(BASE, candidate) const inside = rel && !rel.startsWith("..") && !path.isAbsolute(rel) if (!inside) { res.status(403).send("forbidden") return } // realpath() after existence check also defeats symlink escapes. const real = fs.realpathSync(candidate) if (path.relative(BASE, real).startsWith("..")) { res.status(403).send("forbidden") return } res.send(fs.readFileSync(real)) } // Best: never accept a filename at all — map an id to a known path: // const file = FILES_BY_ID[req.query.id] // allow-list, server owns the path
Limit of the fix: the containment check must run on the canonical (symlink-resolved) path, not the raw input, or a symlink inside the base can still point outside. On platforms with case-insensitive or Unicode-normalising filesystems, canonicalise consistently. An allow-list of identifiers is stronger than any filtering because the server, not the client, chooses the path. Least-privilege filesystem permissions limit what a missed check can reach.
Impact
- ▸Confidentiality: read configuration, secrets, private keys, source code, or other users’ files outside the intended directory.
- ▸Integrity: in write contexts, overwrite files or place files at attacker-chosen paths.
- ▸Code execution: a write into an executed/auto-loaded directory (or zip-slip into one) can escalate to RCE.
- ▸Access-control bypass: reach resources the authorization model assumed were unreachable.
Detection
The structural signal is a taint path from untrusted input to a filesystem sink (open/read/write/serve/extract) where the input forms part of the path and there is no canonicalise-then-contain check between them. Static analysis flags these but cannot always tell a safe allow-list from a raw join. Dynamic testing confirms it: request traversal payloads — plain, URL-encoded, double-encoded, "....//", and absolute — and observe whether an out-of-base file is returned or written. For archives, test entries whose names contain "../" to detect zip-slip.
Where ROOTVIEL fits
This entry is reference material. ROOTVIEL’s verified, execute-and-prove engine is focused on the AI/agentic and Web3 domains, where a finding is confirmed by executing the attack in a sandbox against a deterministic check. Path traversal is an application/code weakness that ROOTVIEL does not currently settle with a verified engine, so a finding here is educational unless it is independently demonstrated against the target.
Support: educational. The input → path → filesystem-call dataflow suits taint analysis, but ROOTVIEL does not promote a verified traversal finding. Treat a match as a hypothesis to confirm with a controlled exploit, not as a proven vulnerability.
Mitigation
Prefer not using the raw filename; where you must, canonicalise then contain. Filtering characters is a last resort.
- ▸Preventive: map a request identifier to a server-owned path via an allow-list; never let the client choose the path string directly.
- ▸Preventive: when a path must be built from input, resolve it to a canonical absolute path and verify it is a descendant of the canonical base directory before any file operation.
- ▸Preventive: resolve symlinks (realpath) and re-check containment to defeat symlink escapes; for archives, reject or re-base entries whose names escape the target directory (zip-slip).
- ▸Defence in depth: run with least-privilege filesystem permissions and, where possible, a chroot/jail or container so out-of-base files are simply not reachable.
- ▸Detective: run static taint analysis and review file-access code for raw joins without a containment check.
Verifying the mitigation
Verify by confirming the resolved path is checked against the base on every file-access path, and that checks run on the canonical form. Then attempt exploitation: send traversal payloads in plain, encoded, double-encoded, "....//", symlink, and absolute forms, and confirm no out-of-base file is read or written. For archive handling, extract a crafted entry with "../" and confirm it stays contained. Report as "these attempts failed against the tested inputs", not as proof of total containment.
A static pass means no unchecked path-building flow was found in the analysed code. Dynamically constructed paths, symlink races (TOCTOU), and newly added endpoints can still escape, so keep probing with encoded and symlink variants.
Common mistakes
- ▸Stripping "../" once — defeated by "....//" and by URL/Unicode/double-encoding.
- ▸Validating the raw input string but checking it before resolution, so symlinks and OS separators slip through.
- ▸Using a path-join helper and assuming it confines the result to the base directory (it does not).
- ▸Fixing the read path but building an upload destination from the attacker-controlled filename.
- ▸Ignoring archive extraction, leaving zip-slip open even when download endpoints are fixed.
References
- [1]MITRE CWE-22 (Path Traversal)
- [2]OWASP Top 10 — A01:2021 Broken Access Control
- [3]OWASP — Path Traversal
- [4]PortSwigger Web Security Academy — Path traversal
References point to public, well-known technical sources for further study.
Related articles
