Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
Home/Blog/What Is Reachability Analysis? Why "Vulnerable" Doesn't Mean "Exploitable"
PlatformAugust 3, 202611 min read

What Is Reachability Analysis? Why "Vulnerable" Doesn't Mean "Exploitable"

Reachability analysis determines whether your code can actually execute a vulnerable function. How static and runtime answer that, and where both fall short.

O
O3 Security Team
What Is Reachability Analysis? Why "Vulnerable" Doesn't Mean "Exploitable"
Key takeaways
  • Reachability analysis asks whether your application code has an actual call path into a vulnerable function, instead of just asking whether the vulnerable package exists in your dependency tree.
  • Static reachability traces call graphs in source code before deployment. Runtime reachability confirms, in a live environment, which of those paths actually execute. Neither is complete on its own.
  • A CVSS score measures how bad a flaw is in the abstract. It says nothing about whether your code gives an attacker a path to it, which is why a medium-severity bug on your request path can outrank a critical one nobody can trigger.
  • Most published vulnerabilities sit in transitive dependencies you never chose, and proving a path through four layers of code you don't own is where most reachability tools stop.
  • Reachability produces false negatives. Reflection, dynamic loading, and plugin architectures defeat static tracing, which is exactly why runtime observation runs alongside it.

Reachability analysis is a technique that determines whether your application code has an actual execution path to a vulnerable function inside a dependency, rather than just confirming the vulnerable package exists somewhere in your tree. That distinction is the difference between a scan that returns 400 findings and one that returns the 12 worth your afternoon. Run npm audit cold on any real project and you'll see the problem. Part 1 of this series covers what reachability means, the two ways it gets measured, and where it honestly falls short.

The alert fatigue loop: the scanner floods the backlog, developers tune it out, security SLAs slip, trust and budget follow, and the next tool bought is cheaper rather than better

The problem: alerts nobody can act on

Every modern application depends on hundreds, sometimes thousands, of open-source packages. Each of those packages can have its own vulnerabilities, and a scanner will happily report every single one it finds.

The result is a familiar loop. Devs get buried under alerts they can't realistically act on, so they start tuning the tool out. Same instinct as muting a Slack channel that won't stop pinging. Meanwhile security's SLAs quietly go red, because "reported" and "fixed" turn out to be very different numbers. Budget follows trust downward, and the tooling the team gets funded to buy next cycle is whatever's cheapest rather than whatever's best. So the noise gets worse, not better.

By the numbers

Black Duck's 2025 Open Source Security and Risk Analysis report covers 1,658 analyses of 965 commercial codebases across 16 industries. It found 86% of those codebases held open-source flaws. 81% held high- or critical-risk ones. When four out of five codebases carry a "critical," the label has stopped sorting anything.

Nobody in this loop is slacking. The loop exists because most scanners are answering a question nobody actually asked: does this CVE exist somewhere in my dependency tree? The answer is almost always yes. Knowing a vulnerability exists isn't the same as knowing what to do about it before lunch.

Why reachability changes the question

Reachability analysis asks a better question: can your code actually reach the vulnerable part of that dependency?

A simple way to think about it. Imagine a package you depend on has a critical vulnerability in its "resize this image" function. If your application never resizes images, if that function is never called anywhere in your code, then the vulnerability is present in your dependency tree but there's no path from your application to it. It can't be triggered through your app, because your app never uses it.

That's the whole idea. Not "is this vulnerability real" (it usually is), but "does my code give an attacker any way to actually hit it." Put another way: vulnerable and exploitable are not the same word. A package is vulnerable the moment a CVE is published against it. It only becomes exploitable in your app if something you wrote can reach the broken function. This single distinction is what turns thousands of alerts into a handful worth acting on. Not by hiding anything. By sorting correctly.

Declared vulnerabilities versus reachable vulnerabilities: only a small slice of a dependency tree has an actual call path from application code into the vulnerable function
Severity and exposure are different axes. A scanner that only measures one of them will consistently get the ordering wrong.

This isn't a new observation. Back in 2018, Pashchenko and colleagues at SAP and the University of Trento studied 10,905 library versions. They covered the 200 most-used open-source Java libraries in SAP's own software. Roughly 20% of the dependencies flagged with a known vulnerability were never deployed at all, so they posed no real danger. That was years before reachability tooling was something you could buy. The gap between "flagged" and "actually exposed" has been measurable for a long time.

Two kinds of reachability, measured at different times

There are two fundamentally different ways to answer "is this reachable," and they answer it at different points in time. Static reachability reads your source code without running it. Runtime reachability watches your application while it runs. They see different things, and they fail in different ways.

Static reachabilityRuntime reachability
What it doesTraces a chain of calls from your entry points down into the vulnerable function. Nothing runs.Watches the live app and sees whether the vulnerable code truly runs.
When it runsEvery commit, before anything ships.Only once the code is deployed and doing real work.
Question it answersCould this run?Did this run, right now, in prod?
Blind spotsReflection, dynamic loading, plugin systems. Anything that resolves only at run time.Paths nobody has exercised yet. Also anything not yet deployed or instrumented.
Cost of being wrongFalse negatives. A path exists, but the trace cannot see it.Late signal. You learn about exposure after it ships.
Static vs runtime reachability: what each one can and cannot tell you

Reachability also isn't a simple yes or no. In practice it's closer to a spectrum. A package might be fully unused, imported but never called, called but not through the exact vulnerable function, or confirmed to run straight through the vulnerable code path. Each of those is a genuinely different level of urgency, even when the underlying CVE and its severity score are identical.

Static reachability traces call paths through source code before deployment; runtime reachability confirms which of those paths actually execute in production

Six misconceptions worth clearing up

The assumptionWhat's actually true
"Not reachable" means it isn't a real vulnerabilityIt means your code doesn't currently expose a path to it. The vulnerability is still real, still worth patching eventually, and reachability changes the moment your code changes.
A high CVSS score means high urgencyCVSS measures how bad a flaw is in the abstract, assuming an attacker can reach it. It knows nothing about whether your application gives them that path.
Reachability is a permanent verdictCode changes. A function nobody called last month might be called today. It has to be re-evaluated continuously, not checked once and forgotten.
Static analysis alone is the full pictureIt's excellent at "could this run," but it has real blind spots: dynamically loaded code, reflection, anything that only resolves at execution time. It can't tell you whether something genuinely happened.
Runtime analysis alone is the full pictureIt only knows about what's already deployed and instrumented. It can't catch anything before it ships, and it says nothing about code paths nobody has exercised yet.
Checking direct dependencies is enoughMost published vulnerabilities don't live in the package you chose. They live several layers down, in something you never directly imported and have no visibility into.
Common assumptions about reachability analysis, and what's actually true

That last one deserves more than a table row. When you install a package, you're not just pulling in that one library. You're pulling in everything it depends on, and everything those depend on, and so on. A typical project might declare 20 to 30 direct dependencies but end up with hundreds, sometimes thousands, of transitive ones sitting underneath, most of which nobody on the team has ever looked at or even knows exist.

This is also where reachability analysis gets genuinely difficult. Tracing "does my code call this vulnerable function" is a single hop when the vulnerable package is a direct dependency. It gets much harder when the vulnerable function is buried four layers into a dependency chain you didn't write and can't see. Now the call graph has to trace through every intermediate layer, function by function, to prove there's an actual path from your code all the way down to the vulnerable line.

Pro tip

Most tools that claim "reachability" only handle the direct-dependency case, because it's the easy 80%. The transitive case, proving or disproving a path through layers of code you don't own, is the harder and more valuable 20%. It's also where most of the real exposure sits. Worth asking any vendor directly: how many hops deep does your call graph actually go?

Real cases, not hypotheticals

Talking about "resize this image" functions is fine for building intuition, but reachability only really lands when you see it applied to CVEs people actually remember panicking about.

Log4Shell (CVE-2021-44228), the one everyone overreacted to, correctly

When Log4Shell was published on December 10, 2021, NIST's National Vulnerability Database rated it CVSS 10.0, as bad as a score gets. The flaw sat in log4j-core: JNDI lookups in log messages and parameters didn't protect against attacker-controlled LDAP endpoints, so anyone who could get a string into a log line could load remote code. Every scanner on the planet flagged every project with Log4j on its classpath. Fair enough, at the time. Nobody had the tooling to say otherwise, so "patch everything, right now" was the correct call.

Run the same event through reachability analysis today and the picture splits hard:

  • Declared: every project with log4j-core anywhere in its dependency tree.
  • Reachable: only the ones whose code actually routes attacker-controlled input into a logger call that hits the vulnerable JNDI lookup path.
  • Safe: everything else. Log4j sitting there, imported, doing normal logging, never touching the vulnerable path.

Work that split against your own fleet and the point lands. Picture 40 services that all ship log4j-core. How many of them actually route untrusted input into a logger call that reaches the JNDI lookup? On most fleets it's a short list. The rest are logging startup banners and request IDs, nowhere near the vulnerable path. Those teams still got the war room, the weekend, and the emergency deploy, because in December 2021 nobody could tell the two groups apart in the moment. The numbers here are illustrative, not measured. Run the trace on your own estate and you'll get your own.

The takeaway isn't that Log4Shell wasn't serious. It was. The takeaway is that the next CVSS 10.0 CVE won't always deserve the same all-hands response, and without reachability you have no way to tell which one is which until you're already in the war room.

The quiet one: a medium-severity CVE sitting directly in the request path

Flip the scenario. A dependency ships a CVSS 5.3, "medium," the kind that gets triaged into a backlog and revisited next sprint, if ever. Standard SCA output puts it below a dozen "critical" findings on the same scan.

Except this one's different. The vulnerable function is a request-parsing helper, and it sits directly on the path every single inbound API call takes before your app does anything else. Static reachability confirms the call graph goes straight from your entry point into the vulnerable function: no branch, no auth check in between, no conditional that skips it. Runtime confirms it's actually firing, dozens of times a second, in production right now.

Severity score: medium. Actual exposure: about as bad as it gets. Internet-facing, unauthenticated, hit on every request. A team triaging by CVSS alone ships three sprints of "critical" fixes before this one even gets looked at.

Key takeaway

This is the failure mode reachability is built to catch. Severity and exposure are different axes, and a scanner that only measures one of them will consistently get the ordering wrong in both directions: panic about unreachable criticals, and sleep through reachable mediums.

The one that never had a CVE: malicious code, not a vulnerability

Not every supply chain incident starts with a CVE. Recent npm and PyPI campaigns have shipped packages that were never "vulnerable" in the traditional sense. They were built maliciously from the start, engineered to exfiltrate environment variables, secrets, or credentials the moment they're imported and executed.

Traditional SCA has nothing to check this against, because there's no CVE database entry to match. This is where reachability's sibling capability matters: watching what a dependency's code actually does at runtime, meaning network calls, file access, and process spawns, rather than only checking its version number against a known-bad list. A package quietly opening an outbound connection to an unfamiliar host the moment it loads is a signal no version-matching scanner will ever surface, because there was nothing published to match it against.

SCA tells you a vulnerability exists. Reachability plus runtime tells you whether it can actually hurt you, and whether it already is. That's the difference between reacting to a scary label and reacting to real risk.

How O3 approaches it

O3 runs both layers together, as a funnel rather than two competing options.

Static analysis narrows the full, unfiltered list of every dependency vulnerability down to what your application's structure could actually execute. That runs cheaply and continuously, on every commit, before deployment, and it traces through transitive layers rather than stopping at your direct dependencies. Runtime analysis then confirms, for what's actually deployed, which of those reachable paths are genuinely being exercised in a live environment, using kernel-level observation via eBPF to watch real execution rather than inferring it.

Static tells you what's possible. Runtime tells you what's real. Together they turn a dependency tree with thousands of theoretical issues into a short list of confirmed, actionable ones, which is what actually breaks the loop at the top of this post.

What's next in this series

Part 2 is a deep dive into static reachability: how the call graph is actually built, what a real reachability trace looks like end to end, and where static analysis draws its confidence boundaries. If you take one thing from Part 1, make it this. Unreachable is a deprioritisation signal, not a dismissal, and the only way to keep it honest is to re-run it every time the code changes.

Frequently asked questions

Does reachability analysis replace SCA, or sit on top of it?

+
It sits on top. SCA still does the job it's good at, identifying every known vulnerability across your direct and transitive dependencies. Reachability takes that full list and tells you which entries your code can actually expose an attacker to. You need the first step to get the second.

Can reachability analysis produce false negatives?

+
Yes, and it's worth being upfront about it. Static analysis can miss paths that only resolve dynamically: reflection, dynamically loaded modules, some plugin architectures. That's exactly why static and runtime are meant to run together. What static analysis can't see ahead of time, runtime observation catches once the code executes.

Does "unreachable" mean I can ignore the vulnerability entirely?

+
No. It means the vulnerability isn't an active risk right now, given how your code currently calls into that dependency. It's still worth patching on a normal cadence, because reachability isn't permanent. A refactor or a new feature can open a path that didn't exist last month. Unreachable is a deprioritisation signal, not a dismissal.

Does reachability analysis work the same way across languages and ecosystems?

+
The underlying idea holds everywhere: trace calls from entry point to vulnerable function. How cleanly it works varies. Statically typed, compiled languages produce cleaner, more complete call graphs. Highly dynamic languages, like Python's duck typing or heavy JavaScript dynamic dispatch, make static tracing harder, which is where runtime confirmation earns its keep.

How is reachability different from a WAF blocking exploit attempts?

+
A WAF reacts to traffic at the edge, looking for what an attack looks like over the wire. Reachability analysis is about your own code's structure and execution paths before any attack happens: which functions exist, which are wired up to run, and which touch a vulnerable dependency. One is a shield at the door. The other tells you which doors lead anywhere.

Do I need production access for reachability analysis to work?

+
No. Static reachability runs entirely against source code, so it works pre-deployment, on every commit, with zero runtime footprint. Runtime reachability needs live execution to observe, but it's additive confirmation rather than a prerequisite. Static alone delivers real value. Runtime is what upgrades "probably fine" into "confirmed, this is firing in prod."

See your full attack chain.
Code, build, runtime. One platform.