A filter that removes one "../" and calls the job done is a filter that loses to two. CWE-27 names that situation: the product builds a pathname from external input and does not neutralize multiple internal "../" sequences, so the path can resolve outside its restricted directory.
MITRE says the "directory/../../filename" manipulation helps bypass some protection schemes, and gives two reasons. A program may remove only one "../" sequence, so multiple sequences get through. Or the manipulation may bypass a check for "../" at the beginning of the pathname, by moving up more than one directory level after a normal looking start.
How it happens
Both bypasses come from one habit: checking the string instead of the destination.
Take the first. A sanitizer replaces "../" with an empty string, once. The input "../../etc/passwd" becomes "../etc/passwd" after one pass, because only the first occurrence was removed. Still dangerous.
Now the second. A check rejects input that starts with "../". The attacker sends "images/../../secret.txt". It begins with "images/", so it passes. The path enters images, climbs out of it, then climbs once more, landing a level above the base directory.
Here is the single removal bug in Python:
def clean(name):
return name.replace("../", "", 1) # removes only the first one
path = os.path.join("/srv/app/files", clean(user_input))
Another common form, in JavaScript:
function clean(name) {
if (name.startsWith("../")) throw new Error("bad path");
return name;
}
const full = path.join("/srv/app/files", clean(req.query.f));
The first is beaten by repetition. The second is beaten by any harmless first segment. In both, the author imagined the attacker would use the most obvious form.
What an attacker gains
The record lists reading files or directories and modifying files or directories.
For observed cases MITRE lists CVE-2002-0298. In that one, a server let remote attackers cause a denial of service through HTTP GET requests containing a %2e%2e (encoded dot-dot), several "/../" sequences, or several "../" in a URI. It is a reminder that repeated sequences can hurt even without file disclosure, and that encoded dots are part of the same family.
How to find it
The record names automated static analysis, which can find some instances by modeling data flow from input into file operations.
A manual review should look for any code that tries to fix a path with string surgery. Warning signs:
- replace or strip calls applied to path input
- checks that test only the start of the string
- regular expressions that look for a single "../"
Testing should use depth. Send one, two, three and four hops, with and without a harmless leading directory name. Also send encoded forms, such as percent encoded dots, since the handler may decode after the check.
If a request with two hops gets through where a request with one is blocked, you have found exactly this weakness.
How to fix it
The record carries no mitigation text. These steps follow from the weakness.
- Stop repairing input. Delete the replace logic. A rule of "reject anything unexpected" is stronger than "clean it and hope".
- Decode first, then check. If the framework decodes after your check, an encoded sequence is invisible to the check.
- Resolve the full path with the platform's resolver, then verify it sits inside the permitted base.
- Compare components, not raw prefixes.
- Prefer IDs. If users pick from known files, accept an identifier and look up the file name yourself.
- Keep the process limited to the folder it serves.
A repaired Python handler:
import os
BASE = os.path.realpath("/srv/app/files")
def open_file(user_input):
full = os.path.realpath(os.path.join(BASE, user_input))
if os.path.commonpath([BASE, full]) != BASE:
raise PermissionError("outside base directory")
return open(full, "rb")
How many hops the attacker uses no longer matters. Whatever the input, the code asks one question about the final location.
The same idea in Java:
Path base = Paths.get("/srv/app/files").toRealPath();
Path full = base.resolve(userInput).normalize();
if (!full.startsWith(base)) {
throw new SecurityException("outside base directory");
}
For links, call toRealPath on the final target after checking it exists.
Quick check
Why does removing "../" once fail?
Each removal handles one sequence. If the input holds several, the rest remain and the path still climbs.
Why can a harmless first folder name beat a start-of-string check?
The check sees a normal beginning and passes it. The climbing happens afterward, so the string looks safe even though the resolved path is not.
Should I decode before or after validating?
Before. Validate the form the file API will actually receive, otherwise encoded sequences can pass the check and be decoded later.