Path traversal happens when user input reaches a filesystem path, letting `../` sequences, absolute paths, symlinks or URL-encoded separators escape the directory you intended. Filtering `../` out of the input does not work — it can be re-introduced by decoding (`%2e%2e%2f`), by nesting (`....//`), or bypassed entirely with an absolute path. The reliable pattern is to resolve the candidate to a canonical absolute path *first* (`os.path.realpath`, `Path.resolve()`, `fs.realpath`, `File.getCanonicalPath`) and then assert that it starts with the base directory plus a separator. Better still, do not accept a path at all: accept an opaque identifier and look the real filename up in a database.
| Languages | Every language with filesystem access; also archive extraction and template loaders |
| Payload shapes | ../, ..\, %2e%2e%2f, ....//, /etc/passwd, C:\Windows\, a symlink pointing outside the base |
| Safe primitives | Path.resolve + is_relative_to, os.path.realpath + commonpath, fs.realpath + startsWith, File.getCanonicalPath, openat with O_NOFOLLOW |
| Typical impact | Reading credentials and keys; arbitrary file write leading to code execution (Zip Slip) |
| Not a fix | input.replace('../', ''), blocklisting '..', or checking the string before normalising it |
| Related | Archive extraction (Zip Slip) is the write-side version of the same bug |
Vulnerable
import os
BASE = "/srv/uploads"
def read(name: str) -> bytes:
# name = "../../etc/passwd" — or "....//....//etc/passwd" after a naive strip
with open(os.path.join(BASE, name), "rb") as fh:
return fh.read()Secure
from pathlib import Path
BASE = Path("/srv/uploads").resolve()
def read(name: str) -> bytes:
# strict=False so a missing file raises FileNotFoundError below, not here.
candidate = (BASE / name).resolve()
if not candidate.is_relative_to(BASE): # Python 3.9+
raise PermissionError("outside upload directory")
if candidate.is_symlink(): # resolve() already followed it; be explicit
raise PermissionError("symlinks are not served")
return candidate.read_bytes()`os.path.join(BASE, name)` returns `name` verbatim when `name` is absolute — that alone defeats a prefix check written before resolution. Resolve, then compare; `is_relative_to` avoids the classic `/srv/uploads-evil` prefix bug that `startswith(BASE)` has.
Vulnerable
app.get("/file", (req, res) => {
res.sendFile(path.join(UPLOAD_DIR, req.query.name));
});Secure
const path = require("node:path");
const fs = require("node:fs/promises");
const BASE = path.resolve("/srv/uploads");
app.get("/file", async (req, res) => {
const raw = String(req.query.name ?? "");
const candidate = path.resolve(BASE, raw);
// The separator matters: without it "/srv/uploads-evil" passes the prefix test.
if (candidate !== BASE && !candidate.startsWith(BASE + path.sep)) {
return res.status(400).end();
}
// realpath resolves symlinks planted inside the base directory.
const real = await fs.realpath(candidate);
if (!real.startsWith(BASE + path.sep)) return res.status(400).end();
res.sendFile(real);
});Express's `res.sendFile` rejects `..` in a relative path but not in a path you already joined, and it does not resolve symlinks for you. `path.resolve` also normalises `%2e%2e` only after your framework has decoded it — decode once, then resolve.
Vulnerable
try (ZipInputStream zis = new ZipInputStream(in)) {
ZipEntry e;
while ((e = zis.getNextEntry()) != null) {
File out = new File(destDir, e.getName()); // e.getName() may be "../../bin/app.sh"
Files.copy(zis, out.toPath());
}
}Secure
Path base = destDir.toPath().toRealPath();
try (ZipInputStream zis = new ZipInputStream(in)) {
ZipEntry e;
while ((e = zis.getNextEntry()) != null) {
Path target = base.resolve(e.getName()).normalize();
if (!target.startsWith(base)) {
throw new IOException("zip entry escapes destination: " + e.getName());
}
if (e.isDirectory()) { Files.createDirectories(target); continue; }
Files.createDirectories(target.getParent());
Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING);
}
}Entry names inside an archive are attacker-controlled strings, not filenames — the ZIP format permits `../`. This is the write-side variant, and it is worse than reading: an entry landing in a cron directory or a web root is code execution.
Each of these is a pull request Orbis AppSec opened against a real open-source repository.
A critical path traversal flaw in `VirtualAdapter.php`'s `resolveMount()` method allowed attackers to escape mounted directory boundaries using sequences like `../../../etc/passwd`. The fix introduces `PathPolicy::normalizeRelative()` to sanitize the remaining path segment before it ever reaches the underlying storage adapter.
TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.
CVE-2026-53951 is a high-severity path traversal vulnerability in Copier 9.15.0 that allowed attackers to bypass trust-prefix checks and execute tasks without user confirmation. Upgrading to Copier 9.15.2 eliminates this attack vector by properly validating file paths before task execution.
A critical command injection vulnerability was discovered in `spider/php/crawler.py` where the `PHPBridge.call()` method passed unvalidated external arguments directly to `subprocess.run()`. An attacker controlling the `spider_path` or `method` parameters could execute arbitrary PHP scripts or inject malicious method names. The fix adds strict input validation — requiring `method` to be a valid Python identifier and `spider_path` to resolve to an existing `.php` file — before any subprocess exec
CVE-2026-27699 is a critical path traversal vulnerability in basic-ftp versions before 5.3.1 that allows attackers to overwrite arbitrary files on the system by crafting malicious file paths. This vulnerability was fixed by upgrading basic-ftp and enforcing strict version constraints across dependent packages. Understanding this attack and its mitigation is essential for developers using FTP libraries in production environments.
A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.
A path traversal vulnerability in `src/server.js` allowed attackers to escape the intended wiki directory by sending encoded traversal sequences through the `/api/pages/:slug(*)` wildcard endpoint. The flawed `startsWith` boundary check could be bypassed after `decodeURIComponent` processing, potentially exposing arbitrary files on the server. The fix replaces the inline filesystem logic with a dedicated `readWikiPage()` function that enforces proper path validation.
The review image upload handler in this Deno-based app trusted the client-supplied filename extension to decide whether a file was a "safe" image, without ever inspecting the actual file bytes. The fix adds magic-byte signature verification for PNG, JPEG, GIF, and WEBP formats before the file is written to disk, closing the door on disguised executables and malicious payloads.
Because the string you filter is not the string the filesystem sees. `%2e%2e%2f` becomes `../` after URL decoding, `....//` becomes `../` after a single naive strip, and `..\` works on Windows. Filtering is also blind to absolute paths, which do not contain `..` at all. Normalise and resolve first, then make one decision about the resolved result.
Not on its own. `/srv/uploads-evil/secret` starts with `/srv/uploads`, so the check passes for a sibling directory. Compare against `baseDir + separator`, or use a path-aware API — `Path.is_relative_to` in Python, `Path.startsWith` in Java, `path.relative` with a leading-`..` test in Node.
Yes, whenever anything else can create files in the base directory — an upload endpoint, a shared volume, another service, or a previously extracted archive. `realpath` before the containment check closes it. On Linux, `openat` with `O_NOFOLLOW` (or `openat2` with `RESOLVE_BENEATH`) enforces it at the syscall level and avoids the time-of-check/time-of-use window entirely.
It bounds the damage, which is worth having, but the interesting files are often inside the container: the application's own configuration, mounted secrets, service-account tokens at `/var/run/secrets`, and `/proc/self/environ`. Containment in code and isolation at the boundary are complementary, not alternatives.
Orbis AppSec scans your GitHub repositories, traces the taint from source to sink, and opens a pull request with the fix applied and verified.
Try Orbis AppSecSee also: File upload security fixes we shipped