Each sweet has an allergen sheet you can download. The shop builds the file path by tacking
your filename onto its allergens folder and reading whatever comes out. Because it never
checks where that path actually points, a few ../ steps walk out of the folder
and read files the shop never meant to hand over. This is path traversal.
New to this? What is path traversal?
Path traversal (or directory traversal) is when a filename you supply is
used to build a path on the server without confining it. The special sequence
../ means “up one folder”, so enough of them climb out of the
intended directory.
From there you can read configuration, source code, secrets, or system files like
/etc/passwd. The fix is to resolve the final path and confirm it still sits
inside the folder you meant.
Make sure the shop is running at http://localhost:3000.
No account needed.
1 · Find it
On a sweet’s page, click Allergen sheet. Look at your browser’s
address bar: the download is served from a URL that names the file at the end,
after sheet=:
http://localhost:3000/allergens?sheet=rhubarb-and-custard.txt
The sheet parameter is just a filename, handed straight to the server. That is
the thing to poke at: change the value after sheet= and press Enter.
2 · Exploit it
Point it up and out of the allergens folder with ../ (which means “go up one
folder”). Put this in the address bar:
http://localhost:3000/allergens?sheet=../package.json
Back comes the app’s package.json, which lives outside the allergens
folder entirely. Add more steps to climb further, all the way to system files:
http://localhost:3000/allergens?sheet=../../../../../../etc/passwd Why it matters: the same trick reads source code, environment files, keys and credentials. A read-only file bug quickly becomes a full compromise once it finds a secret.
3 · Understand it
The handler joins your filename onto the allergens directory and reads it. Joining does not
stop ../, so the final path can land anywhere:
app.get('/allergens', (req, res) => {
const file = path.join(allergensDir, req.query.sheet);
res.send(readFileSync(file, 'utf8')); // wherever that points
}); path.join(allergensDir, '../package.json') quietly resolves to a file one level
up. The code trusted the filename to stay put. It did not.
4 · Fix it
Resolve the final path and confirm it is still inside the allergens folder before reading. If it escaped, refuse:
const file =
path.join(allergensDir, sheet);
res.send(readFileSync(file)); const file =
path.resolve(allergensDir, sheet);
if (!file.startsWith(allergensDir + path.sep)) {
return res.sendStatus(400);
}
res.send(readFileSync(file));
Now ../package.json resolves to a path outside the folder, the check fails, and
the shop hands back nothing but a polite refusal.
The general rule: never build a file path straight from user input. Map an id to a known filename where you can, and when you must take a name, resolve it and confirm it stays inside the intended directory. Reject anything that climbs out.