Parameter

Reachability analysis

Also known as

  • Vulnerability reachability
  • Function-level reachability

Reachability analysis is a technique for triaging dependency vulnerabilities that checks whether your application can actually execute the vulnerable function in a library, by tracing a call path from your own code to it, so teams fix the findings that are reachable and deprioritize ones where the flawed code is present but never runs.

Category
Supply chain
Last reviewed

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:

LevelQuestion it answersTypical result
Package presentIs the vulnerable version in the lockfile or image?Flags everything, including dev and unused transitive packages
Package importedDoes any of my code import the vulnerable package or module?Drops packages that are installed but never loaded
Function reachableIs 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 === true

So 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, Python getattr and importlib, JavaScript require(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:

  1. Reachable and in KEV: fix now.
  2. Reachable with a high EPSS score, or reachable from an unauthenticated entry point: fix in the current cycle.
  3. Reachable, low EPSS, internal-only path: schedule it.
  4. Not reachable: record the reasoning, often as a VEX statement with the justification vulnerable_code_not_in_execute_path alongside 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.

Written by Parameter · Last reviewed