What does "reachable" mean?
A vulnerability is reachable when there is a path of calls from your application's code to the specific function that contains the flaw. Most dependency scanners stop earlier: they see that a vulnerable version of a package is installed and report it, whether or not your code ever touches the broken part.
There are three levels of precision, and tools that say "reachability" can mean any of them:
| Level | Question it answers | Typical result |
|---|---|---|
| Package present | Is the vulnerable version in the lockfile or image? | Flags everything, including dev and unused transitive packages |
| Package imported | Does any of my code import the vulnerable package or module? | Drops packages that are installed but never loaded |
| Function reachable | Is there a call path from my code to the vulnerable function? | Drops imported packages whose flawed function is never called |
Function-level reachability needs to know which function is vulnerable, and the CVE record usually does not say. The Go vulnerability database is the clearest public example of that data: each entry's ecosystem_specific.imports lists the package path and the vulnerable symbols, and the govulncheck tool uses them to report only vulnerabilities where your code is transitively calling a vulnerable function. For other ecosystems, commercial tools curate the same function-level data themselves, and its quality varies by tool.
What does a worked example look like?
Take an invented Node.js service, demo-app, with a dependency on an invented Markdown library, demo-markdown@3.2.0. An advisory says versions below 3.2.1 have a regular-expression denial-of-service flaw in parseTable() and an HTML-escaping flaw in renderRawHtml(), which only runs when the allowHtml option is on.
The app's own code:
// src/comments.js
import { Markdown } from "demo-markdown";
const md = new Markdown({ allowHtml: false });
export function formatComment(body) {
return md.render(body);
}A call graph from the app's entry points gives:
POST /api/comments
routes/comments.js: createComment()
src/comments.js: formatComment()
demo-markdown/lib/index.js: Markdown.render()
demo-markdown/lib/block.js: parseBlocks()
demo-markdown/lib/table.js: parseTable() <-- vulnerable, REACHABLE
demo-markdown/lib/html.js: renderRawHtml() <-- vulnerable, no path found
only called from Markdown.render() when options.allowHtml === trueSo the same package has one reachable and one unreachable vulnerability. The parseTable() path starts at a public route and carries text any commenter controls, which is what makes it worth fixing now. The renderRawHtml() path depends on an option the app sets to false, a constant a good static analyzer can see through and a weaker one cannot, which is why tools disagree on findings like this one.
A reachable finding is not a proven exploit. The next step is to confirm that input from the route actually arrives at parseTable() in a form that triggers the flaw, ideally with a harmless test case in a staging environment.
Static vs runtime reachability
Static reachability builds a call graph from source or bytecode without running anything; runtime reachability records which functions actually load or execute in a running service. They fail in opposite directions.
- Static sees every path the code could take, including rare ones that tests never exercise. It over-reports when it cannot resolve indirect calls (it assumes they might go anywhere) and under-reports when calls are invisible to it.
- Runtime (an agent, profiler or instrumented build in staging or production) sees only what executed during the observation window. Code that runs once a quarter, on an error path, or only for one tenant's configuration looks unreachable when it is not.
Used together, runtime data can confirm a static path is live, and static analysis covers the paths the runtime sample missed.
Where does reachability analysis break down?
Anywhere the target of a call is decided at runtime. The govulncheck documentation is candid about this: it analyzes function-pointer and interface calls conservatively, which can produce false positives, and calls made through the reflect package are invisible to its source analysis, so vulnerable code reachable only that way goes unreported. The same classes of problem show up in every language:
- Reflection and dynamic loading. Java reflection,
Class.forName, Pythongetattrandimportlib, JavaScriptrequire(variable). - Dynamic dispatch. Interface and virtual method calls, callbacks, event emitters and dependency-injection containers, where the analyzer must guess which implementation runs.
- Framework entry points. Routes, serializers, annotations and middleware registered by convention; if the tool does not model the framework, whole request paths start nowhere.
- Deserialization and configuration. A vulnerable class loaded because a YAML file or a serialized object names it.
- Non-code exposure. Some flaws do not need a function call from your code at all: a vulnerable server component that listens on a port, or a flaw triggered at import or install time.
- Binary-only analysis. The govulncheck docs note that binaries lack detailed call information, so in binary mode it reports matching symbols without call stacks.
Treat "unreachable" as "deprioritized with a stated reason," never as "closed." Revisit it when the code changes, since one new call site makes it reachable.
How does reachability relate to EPSS and KEV?
They answer different questions, and good triage uses all three. Reachability asks whether your application can run the flawed code. EPSS estimates the probability that the CVE will be exploited in the wild in the next 30 days, and the KEV catalog records that it already has been. CVSS describes the severity if it is exploited.
A practical ordering:
- Reachable and in KEV: fix now.
- Reachable with a high EPSS score, or reachable from an unauthenticated entry point: fix in the current cycle.
- Reachable, low EPSS, internal-only path: schedule it.
- Not reachable: record the reasoning, often as a VEX statement with the justification
vulnerable_code_not_in_execute_pathalongside the SBOM, and upgrade during routine dependency updates.
KEV status can override an "unreachable" verdict when you have low confidence in the call graph, for example in a reflection-heavy Java service.
[ Sources ]
- Go: govulncheck command documentation, including Limitations
- Go vulnerability database: ecosystem_specific imports and symbols fields
- Go tutorial: Find and fix vulnerable dependencies with govulncheck
- OpenVEX specification: status justifications
- FIRST: Exploit Prediction Scoring System (EPSS)
- CISA: Known Exploited Vulnerabilities Catalog
Written by Parameter · Last reviewed

