Path traversal shows up wherever a filename or path segment travels from a request into a filesystem call without first being confined to the directory it's supposed to stay in. The classic ../../../etc/passwd payload is what security scanners look for, but real-world traversal bugs are usually quieter than that — a single ..that walks a download endpoint one directory above a user's own upload folder is enough to leak another tenant's files.
Where user input meets a filesystem path
- File download or preview endpoints that take a filename, document ID, or template name as a parameter and join it onto a base directory.
- Static file servers with a custom route for user-uploaded content, rather than a dedicated object store with its own access controls.
- Template or theme engines that let a user select a template by name, which then gets read from disk.
- Archive extraction — a zip, tar, or similar upload whose internal entry names get written to disk using the paths stored inside the archive itself.
- Log file viewers or export features in admin panels that accept a log name or date as a path component.
Why blocking ".." isn't enough
A surprising number of fixes stop at stripping the literal string .. from the input, which is trivially bypassed with URL encoding (%2e%2e%2f), double encoding, backslashes on Windows-hosted services, or null-byte tricks on older runtimes. The reliable fix isn't pattern matching on the input string at all — it's resolving the final path (path.resolve or equivalent) and then checking that the resolved absolute path still starts with the intended base directory before touching the filesystem. Reject anything that resolves outside that boundary, rather than trying to sanitize the input into looking safe.
Archive extraction is a traversal vector too
"Zip slip" is path traversal wearing a different hat: a malicious archive contains an entry named something like ../../../../home/user/.ssh/authorized_keys, and a naive extraction loop writes each entry to outputDir + entry.name without validating that the joined path stays inside outputDir. Any feature that unzips user-supplied archives — theme uploads, bulk import, report bundles — needs the same resolved-path check as a direct file-read endpoint, applied per entry during extraction.
Symlinks complicate the resolved-path check
Resolving a path and confirming it starts with the base directory handles ..segments, but a symlink that was placed inside the base directory ahead of time can still point somewhere else entirely. For upload directories where users control filenames, either resolve through symlinks (most path resolution functions do this by default) before the boundary check, or reject symlinks in user-writable directories outright if the feature doesn't need them.
Checklist for reviewing a diff that reads or writes files by name
- The final path is resolved to an absolute path and checked against the intended base directory — not just checked for the substring
... - Archive extraction validates each entry's resolved output path individually, not just the archive's own filename.
- Where possible, the endpoint maps a user-facing identifier to a path server-side (a database lookup by ID) rather than trusting a path fragment from the client at all.
- File read/write permissions on the process are scoped as narrowly as the deployment allows, so a traversal bug that slips through still can't reach unrelated system files.