Serving a file by name is a common feature: a download link, a file viewer, an attachment fetcher. When an assistant writes one, it joins the requested name onto your files folder and serves whatever is there, which works perfectly for a normal filename. The gap is that a filename can contain ../ sequences, and those tell the filesystem to climb up out of your folder.
How the bug works
The request supplies a name, your code joins it to the intended directory, and the result is used to read a file. If the name is a real filename, it stays in the folder. If the name is a series of ../ segments followed by a path, it resolves to somewhere else entirely, and the server happily reads it.
// the vulnerable pattern: join a name from the request and serve it const file = path.join(uploadDir, req.query.name); res.sendFile(file); // a crafted name climbs out of uploadDir: // ../../../../etc/passwd // and the server returns a file it should never expose
What an attacker reaches this way is anything the process can read: system files, your configuration and .env, private keys, source code. Because those files hold secrets, a single traversal bug can leak far more than the folder you meant to share, and it pairs badly with any secret sitting in a config file.
Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →The fix: resolve, then confirm it stayed inside
The reliable fix is to resolve the final absolute path and check that it still sits within your intended directory before you use it. Resolving normalizes the ../ sequences, so you can see where the path actually lands and reject anything that escaped.
const base = path.resolve(uploadDir);
const p = path.resolve(base, req.query.name);
if (!p.startsWith(base + path.sep)) {
return res.status(400).json({ error: "Invalid path" });
}
res.sendFile(p);Now a traversal attempt resolves to somewhere outside base, fails the check, and is rejected. The full walkthrough is in the fix for path traversal from user input. Note the shape of the fix: rather than stripping dangerous sequences, which is easy to get wrong, it verifies where the path resolves, the same structural approach that beats filtering in command injection.
Frequently asked questions
What can an attacker actually read with path traversal?
Anything the server process can read. By sending ../ sequences to climb out of your intended folder, they can reach system files like the password file, your application's configuration and .env, private keys, and source code. Because these files often contain credentials and secrets, one traversal bug in a file-serving endpoint can expose far more than the folder you meant to share.
Doesn't joining the path safely handle this?
A plain join does not, because the join happily includes the ../ segments and resolves to a location outside your folder. The safe approach is to resolve the final absolute path and then verify it still sits inside the directory you intended, rejecting it if not. Resolving normalizes the ../ sequences so you can check where the path really lands, which a simple concatenation never does.
Is stripping ../ from the input enough?
Stripping is fragile and tends to miss cases: encoded forms, backslashes on some systems, and sequences that reappear after one pass of removal. The reliable fix is to resolve the full path and confirm it is still within the base directory, rather than trying to blocklist the dangerous sequences. Check where the path resolves to, do not try to sanitize the input into safety.
Where does this show up most in AI-built apps?
In any endpoint that serves or reads a file by a name from the request: a download route, a file viewer, an avatar or attachment fetcher, a template loader. The assistant builds the direct version that joins the name onto a folder and serves it, which works for normal filenames and only misbehaves when the name contains traversal sequences the author never tested.
Check your file-serving routes
Any endpoint that builds a file path from a request value is worth a look, and a scan flags the pattern by shape so you do not have to find every one by hand. A scan reads the repo and points at the line, so you can add the containment check before someone sends ../ instead of a filename. Run a free scan and see whether any route serves a file by an unchecked name.