Skip to contentExploitQuest

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

Wrenlearner

So I just strip out .. before I use the filename?

Rookmentor

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
  1. 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.
  2. 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?