The bug
Here's the route from a small Express demo app
(scan-target-demo-apps/apps/06-path-traversal):
app.get('/files', (req, res) => {
const name = req.query.name;
const filePath = path.join(docsDir, name);
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
return res.status(404).json({ error: 'file not found', path: filePath });
}
res.type('text/plain').send(data);
});
});
name comes straight from the query string and goes straight into path.join() — no check that the
resolved path actually stays inside docsDir. path.join('./docs', '../secret.txt') happily
resolves to a file outside ./docs. Request /files?name=../secret.txt and the server reads and
returns whatever's sitting one directory above the one it's supposed to be confined to.
Classic CWE-22, OWASP A01:2021 (Broken Access Control). Nothing novel in the bug itself — the
interesting part, again, is what caught it and how many independent ways.
What actually caught it — twice, independently
We ran this target through AI Security Studio, an offline security research platform we've been
building. No cloud calls, no code leaves the machine.
This is the first target in the series where the bug is confirmed by both ends of the pipeline
at once, not just one:
1. Static — before anyone opens the app. The same JavaScript taint rule that flags command
injection also declares a filesystem sink: fs.readFile / readFileSync / writeFile /
.sendFile(, etc. It traces req.query.name → path.join(docsDir, name) → fs.readFile(filePath, entirely inside
…)server.js, and auto-creates a finding — Risk: High, CWE-22, OWASP A01:2021 —
with zero manual steps. This scores reachability: untrusted input reaches a filesystem sink.
2. Dynamic — a live, automated probe. Unlike some other bug classes in this series where the
active-probe side isn't wired up yet, path traversal has a real, on-by-default probe
(PathTraversalProbeAdapter, ProbeSafetyTier.ReadOnly). Point it at /files with parameter name
and run it: it comes back with a Critical finding, Confidence: Likely, and — the part that
usually takes a human write-up — an attached, ready-to-run curl reproduction already generated. This
scores confirmed exploitation, which is why it lands at a higher risk level than the static-only
finding for the same root cause. Different numbers for the same bug at different pipeline stages is
expected, not a bug in the platform.
Proving it by hand needs nothing but a browser, too — no tooling required:
GET /files?name=../secret.txt
returns the contents of secret.txt, a file that lives one directory above the app's intended
./docs root and was never meant to be reachable through /files at all.
Why "offline" is the actual point
- You can point this at unreleased/internal code without a vendor's cloud pipeline ever touching it.
- The rule engine and the active-test probe — not the LLM — do the actual finding and the actual exploitation. Results are reproducible and don't drift between runs.
- The local LLM's job shrinks to something it's actually reliable at: turning the already-structured
evidence (the tainted source line, the curl proof, the CWE/OWASP references) into a plain-language
explanation — "untrusted input reaches a filesystem read via unsanitized
path.join, allowing arbitrary file disclosure outside the intended directory." No raw request/response bodies, no raw source files, ever get handed to the model directly.
Try it yourself
The demo app is intentionally vulnerable and open source — safe to point any scanner at, including
this one:
Try it yourself → https://github.com/sendwavehub/scan-target-demo-apps
Windows Store https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US
Web Site https://Sendwavehub.tech/en/apps/ai-security-studio-4
- Video walkthrough:
Built for security researchers, pentesters, and AppSec teams doing work they're authorized to do.
Top comments (0)