Lesson 3 of 3 in Path Traversal
Keep It In The Folder
The fix is not a blocklist of dangerous strings — attackers have more encodings than you have filters. It is to resolve the path and refuse anything that lands outside the folder, or to never build a path from input at all.
2 min read
Not yet reviewed
So I just strip out .. before I use the filename?
That is the fix everyone reaches for, and it is the one attackers are most ready for. Stripping strings is a race you lose. Resolving the path is a race you do not have to run.
Resolve, then confine
The reliable fix does not inspect the string the attacker sent. It resolves the full path — letting every .. do its worst — and then checks whether the result is still inside the folder it was supposed to stay in. If it escaped, refuse.
Filtering the string (loses)
file = file.replace('..', '')
read(join(dir, file))Resolving and confining (wins)
const full = resolve(dir, file)
if (!full.startsWith(resolve(dir) + sep)) return forbidden()
read(full)The filter on the left is defeated by ....// (strip one .. and .. remains), by URL-encoding, by absolute paths. The check on the right does not care how the path was written — it asks the only question that matters: after resolving, are we still inside the folder?
The stronger fix
Better still, do not build a filesystem path out of user input at all. If the application knows the set of files it will ever serve, let the user pick an id from that set and map it to a path yourself. An input that is never a path cannot traverse one.
The non-fixes
Two habits feel like fixes and are not, for the same reason the SQL keyword-blocklist is not: they filter the symptom and the attacker changes the spelling.
strip "../" ....// survives it; so does %2e%2e%2f
blocklist filenames a blocklist of bad paths is infinite and you will miss one
- Line 1Removing
../once leaves..in....//, and the browser will happily send%2e%2e%2f, which decodes to../after your filter ran. You are matching spellings of an idea; there are always more. - Line 2You cannot enumerate every dangerous path, because the dangerous set is "everything outside the folder" — which is unbounded. Confinement names the safe set instead, which is small and known.
Why is "resolve then check the prefix" safe when "strip the string" is not, if both are just code the developer writes?
.., ., doubled slashes, symbolic detours — into one canonical answer, and then a single comparison decides it. The string filter has to anticipate every *spelling* of an escape; the resolve-and-confine check only has to know the *destination*, and the destination is computed by the same rules the filesystem uses.