CWE-26 describes path traversal through input shaped like "/dir/../filename". The product builds a pathname from external input, expects it to stay inside a restricted directory, and does not neutralize the embedded "../" that moves it somewhere else.
MITRE calls the manipulation useful for bypassing some protection schemes. The scheme it has in mind is a check that only looks at how the input begins.
How it happens
Suppose a download endpoint is meant to serve files under an uploads folder. The developer adds a rule: the requested name must start with the uploads prefix. A request for "/data/upload/report.pdf" passes. So does "/data/upload/../../etc/passwd", because it starts with the right text.
The check confirmed the beginning of the string. It never asked where the path ends up after the operating system resolves the dots.
This is the pattern in two observed cases from MITRE:
- CVE-2025-25295: a Python SDK checks whether a URL starts with "/data/upload/" but still allows "/data/upload/../../" traversal sequences, which are then handled by os.path.join.
- CVE-2023-25802: a web interface allows reading arbitrary files through manipulations such as "/etc/nginx/../passwd".
The first is a prefix check beaten by dots. The second shows how a plausible looking directory in the middle disguises the climb.
What an attacker gains
The record lists reading files or directories and modifying files or directories. Reading exposes configuration files, keys and source. Writing can replace programs or data the application trusts.
How to find it
Static analysis can trace external input into file operations and flag paths that are used without normalization. In code review, look for startswith or similar prefix tests applied to a raw string that is later opened as a file. That pairing is the classic shape.
When testing, include a legitimate looking prefix followed by climbing sequences, and try several levels of climbing.
How to fix it
Resolve the path, then compare it against the allowed base. Do this in that order.
import os
UPLOADS = os.path.realpath("/data/upload")
def open_upload(name):
full = os.path.realpath(os.path.join(UPLOADS, name.lstrip("/")))
if os.path.commonpath([UPLOADS, full]) != UPLOADS:
raise PermissionError("outside upload directory")
return open(full, "rb")
Two details matter. realpath collapses the dots and follows links, so the comparison sees the true location. And commonpath compares whole path components, which avoids the trap where "/data/upload2" shares a text prefix with "/data/upload".
Beyond that:
- Where you can, accept an ID and look up the file yourself, so the client never sends a path.
- Run the service with access only to the directories it needs.
- Keep the SDK or library that handles paths up to date, since the first case above lived in a library.
Quick check
Why is a startswith check on the raw string not enough?
It tests text, not location. Dots later in the string can move the final path out of the directory while the start still looks right.