A path traversal bug rarely announces itself. It looks like ordinary file-handling code — a filename pulled from a query parameter, concatenated onto a base directory, then passed to a read or write call. There's no obviously dangerous function, no string that screams "vulnerable." The danger only becomes visible when you trace where the filename actually comes from and whether anything strips out sequences like ../ before the file system ever sees them. That tracing problem is exactly what static analyzers historically struggled with, because grep-style pattern matching can flag fs.readFile() calls all day without ever knowing if the path argument is attacker-controlled. Snyk Code, the SAST engine built into Snyk's platform, approaches this differently: it models how data actually flows through a program — across functions, files, and conditional branches — before deciding whether a sink is reachable by untrusted input. It's one of the more instructive examples of how Snyk vulnerabilities get flagged with real reasoning attached, rather than a bare pattern match. Here's how that mechanism works, and where a path traversal check like it fits into a broader supply chain security program.
What makes path traversal hard for static scanners to catch?
The difficulty is that path traversal (tracked as CWE-22) is a data-flow vulnerability, not a syntax vulnerability — the code that causes it is syntactically identical to the code that's safe. Consider:
app.get('/download', (req, res) => {
const filename = req.query.file;
const filePath = path.join('/var/app/uploads', filename);
res.sendFile(filePath);
});
Nothing here is inherently wrong. path.join and res.sendFile are used constantly in legitimate code. The vulnerability only exists because req.query.file — user-controlled input — reaches sendFile without validation, so a request for ?file=../../../../etc/passwd walks out of /var/app/uploads entirely. A tool that only pattern-matches on "dangerous functions" like sendFile, readFile, or File() will either miss the real bugs or bury them under false positives on the safe cases, because the function name alone tells you nothing about the argument's origin.
How does Snyk Code trace a variable from input to file access?
It builds a data-flow graph that connects known taint sources to known sensitive sinks, then checks whether the path between them is sanitized. Snyk Code performs interprocedural analysis, meaning the trace isn't limited to a single function — it follows a variable as it's passed as an argument, reassigned, destructured, or returned across multiple functions and even multiple files in the same project. In practice this means the source (req.query.file, req.params.name, process.argv, an environment variable, a deserialized JSON field) can be many call-frames away from the sink (fs.readFile, path.resolve, File.newInputStream, ZipFile.getInputStream), and the analysis still connects the two if no sanitizing logic sits in between. Snyk's own published research materials describe this as symbolic/semantic analysis over the program's abstract syntax tree combined with a knowledge base of source-sink-sanitizer relationships trained on a large corpus of open source code and historical vulnerability fixes — the same DeepCode AI engine and code-scanning infrastructure Snyk brought in through its 2020 acquisition of DeepCode. The result is a flow-sensitive check: the tool isn't just asking "does dangerous input exist" and "does a dangerous function exist," it's asking "does a path connect the two, and is that path clean."
How does it decide a sanitizer actually neutralizes the taint?
It checks whether the sanitizing function is a recognized one and whether it sits correctly in the flow, not just whether some validation-looking code exists nearby. For path traversal specifically, recognized sanitization patterns generally include normalizing the path (resolving .. and symlink segments) and then verifying the resolved absolute path still starts with the intended base directory — for example:
const resolved = path.resolve(baseDir, filename);
if (!resolved.startsWith(path.resolve(baseDir))) {
throw new Error('Invalid path');
}
If a developer instead does a naive check — say, rejecting the literal substring ../ without resolving the path first — that's insufficient, because encoded traversal sequences (..%2f, ..\/, double-encoding, or absolute paths that bypass a relative-prefix check entirely) can slip past a substring filter while still escaping the directory. A data-flow-aware scanner can flag that the "sanitizer" exists in the code but doesn't structurally break the taint from source to sink, whereas a naive pattern-matcher would see the word "sanitize" or a regex check nearby and stop flagging the finding — a common source of false negatives in shallower tools.
Why does interprocedural, cross-file tracing matter more for path traversal than for other bug classes?
Because path-handling logic is disproportionately likely to be split across a request handler, a utility module, and a file-access wrapper — three separate files is a completely normal layout for a Node.js, Java, or Python backend. A single-file or single-function scanner will lose the taint the moment the variable crosses a file boundary, which is precisely why many home-grown regex-based checks and early-generation SAST tools produce a high false-negative rate on this vulnerability class. Real-world path traversal incidents bear this out: the disclosure of Apache HTTP Server's CVE-2021-41773 in October 2021 (and its incomplete-fix follow-up CVE-2021-42013 weeks later) involved path normalization logic that failed on specific encoded sequences, and it was serious enough to be added to CISA's Known Exploited Vulnerabilities catalog. On the archive-extraction side, Snyk's own security research team publicly disclosed the "Zip Slip" vulnerability class in 2018 — a directory traversal flaw found in the extraction logic of dozens of libraries across Java, JavaScript, .NET, Go, and Ruby, where a malicious filename inside a zip/tar archive could write files outside the intended extraction directory during decompression. Both cases show the same shape: the vulnerable logic is a multi-step transformation of a filename, not a single obviously bad line.
Where does this fit into a developer's actual workflow?
Snyk Code runs as both an IDE extension (VS Code, IntelliJ, and others) that flags issues as code is typed, and as a check in CI/PR pipelines that gates or annotates pull requests, with results also exportable in SARIF format for aggregation elsewhere. Because the analysis is designed to run quickly enough for interactive use, findings are typically presented with the full traced path highlighted — showing the exact source line, each intermediate step the tainted variable passed through, and the sink line — so a reviewer isn't just told "path traversal possible," but can see the specific hops that make the flow exploitable. This traced-path presentation is what separates a data-flow finding from a bare pattern-match alert: it gives the developer the reasoning, not just the verdict.
Does symbolic/data-flow tracing eliminate false positives entirely?
No — flow-sensitive analysis reduces false positives on unsanitized-looking code that's actually safe, but it doesn't make static analysis perfect, because any static tool has to make approximations about dynamic behavior it can't fully observe at scan time (reflection, dynamically constructed strings, framework-level input handling it doesn't model, third-party sanitizers it doesn't recognize). Snyk publishes guidance for triaging and marking findings as false positives within its platform precisely because analysts are expected to review flagged flows rather than treat every alert as confirmed-exploitable. That's true of any SAST approach, symbolic or otherwise — the value of data-flow tracing is that it narrows the review burden to flows that are genuinely plausible, rather than every occurrence of a sensitive function name in the codebase.
How Safeguard Helps
Path traversal is a textbook example of why supply chain security can't stop at cataloging known Snyk vulnerabilities or checking whether a dependency's version is affected by a published CVE. A large share of these flaws live in first-party application code and in how a project glues its own request handlers to file-system calls — the exact surface SAST tools like Snyk Code are built to trace. Safeguard's platform is built around giving security and engineering teams visibility across that whole surface: correlating SAST findings, dependency and SBOM data, and build/release provenance in one place so a CWE-22 finding in a PR isn't reviewed in isolation from what package it ships in, what environment it deploys to, or whether the affected code path is actually reachable from production traffic. Rather than replacing a data-flow-aware scanner, Safeguard's job is to make sure findings like these are triaged with full context — ownership, exploitability signals, and downstream blast radius — so teams can prioritize the traversal bug that touches an internet-facing upload handler over the one buried in a dead code path, and can prove to auditors and customers that the finding was tracked from detection through remediation as part of a documented software supply chain security program.