What it is#
Path traversal occurs when an application uses untrusted input as part of a filesystem path without reliably confining the result to an approved directory. Dot segments, absolute paths, encoded separators, symbolic links, archive entries, or platform-specific path rules can escape the intended location.
The flaw is not limited to downloads. It can affect uploads, log viewers, template selection, archive extraction, backup restoration, file deletion, static-file handlers, and any feature that reads or writes a path.
Normalization matters. Filtering the literal text ../ is not sufficient because decoding, Unicode handling, Windows separators, nested encodings, and filesystem links can change the path after a check.
Why it matters#
- Read source code, configuration, credentials, keys, logs, or other users' files.
- Overwrite application files, startup configuration, scheduled jobs, or authorized-key material.
- Delete files outside the intended workspace.
- Extract malicious archive entries outside the destination directory, often called Zip Slip.
- Cross tenant boundaries when customer-controlled directories share a filesystem.
- Escalate to code execution when a writable path is later loaded or executed.
Medium for limited disclosure of low-sensitivity files. High to critical when secrets can be read or executable, configuration, or authentication-related files can be written. Actual severity depends on context: who is affected, what data or actions are reachable, and existing controls.
Examples#
// Vulnerable: the filename can leave the reports directory.
import path from 'node:path';
app.get('/reports/download', requireLogin, (req, res) => {
const filename = String(req.query.file);
res.sendFile(path.join('/srv/reports', filename));
});
// Fixed: expose an identifier, then look up a server-owned storage name.
app.get('/reports/:id/download', requireLogin, async (req, res) => {
const report = await db.report.findFirst({
where: { id: req.params.id, accountId: req.user.accountId },
select: { storageKey: true, downloadName: true },
});
if (!report) return res.sendStatus(404);
// storageKey was generated by the server, not supplied as a path by the user.
res.download(`/srv/reports/${report.storageKey}`, report.downloadName);
});
If a path must be accepted, resolve it against a fixed root and verify the final canonical path remains beneath that root. That check must also account for symbolic links and race conditions.
How to fix it#
1. Do not expose filesystem names. Use an opaque record ID and map it to a server-generated storage key after checking ownership and authorization.
2. Generate destination names. For uploads, create random server-side names and store the original display name only as metadata.
3. Canonicalize once, then enforce containment. Decode at a defined boundary, resolve the complete path, and verify it is inside the expected root using path-aware functions. Include a trailing path separator in prefix comparisons.
4. Reject absolute and ambiguous paths. Disallow dot segments, alternate separators, drive letters, UNC paths, null bytes, device names, and unexpected Unicode forms according to the target platform.
5. Defend against symbolic links. Open files using platform controls that avoid following links where available, verify file descriptors rather than only names, and keep attacker-writable directories separate from trusted data.
6. Extract archives safely. Inspect every entry after normalization, reject absolute paths and links, enforce a single extraction root, and limit expanded size, file count, and nesting.
7. Use least filesystem privilege. The application should not be able to read unrelated secrets or write executable and configuration directories.
8. Apply access control independently. A safe path does not prove the caller may access the file. Check tenant, owner, role, and purpose before opening it.
How to test for it#
- Inventory all file reads, writes, deletes, downloads, uploads, archive operations, template selection, and log-viewing features.
- Use a dedicated harmless canary file immediately outside a test directory and confirm it cannot be reached.
- Test dot segments, absolute paths, mixed separators, repeated encoding, trailing dots or spaces, Unicode normalization, and Windows-specific forms where applicable.
- Test symbolic links and archive entries in an isolated fixture directory; do not target operating-system or production files.
- Verify that path checks occur after the application's final decoding and before the filesystem operation.
- Confirm that valid files belonging to another test user or tenant remain inaccessible even when their identifiers are known.
- Add unit tests for normalization and containment on every supported operating system, plus integration tests against the real storage backend.
References#
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ↗ (opens in a new tab)cwe.mitre.org
- OWASP Path Traversal ↗ (opens in a new tab)owasp.org
- OWASP Top 10 A01:2021 Broken Access Control ↗ (opens in a new tab)owasp.org
- PortSwigger Web Security Academy: Directory traversal ↗ (opens in a new tab)portswigger.net