Is That CVE Reachable? Triaging Dependency Alerts Without Guessing
Published · Strata Security
On This Page
- The Problem With a Dependency Alert List
- What Reachability Actually Means
- Question 1: Is the Vulnerable Code Even in Your Build?
- Question 2: Do You Use the Vulnerable Feature?
- Question 3: Can Untrusted Input Reach It?
- Three Worked Examples
- Writing Down What Would Change the Answer
- What Reachability Analysis Is Not
- Recording the Result So It Survives
- Reachability Triage Checklist
- Frequently Asked Questions
- Conclusion
1The Problem With a Dependency Alert List
A dependency scanner reads your lockfile, matches package names and versions against a vulnerability database, and returns a list. The list is accurate about what it claims: these versions of these packages have published advisories. What it cannot tell you — and does not claim to — is whether any of those advisories describe something an attacker can actually do to your application.
That gap produces two failure modes, and teams tend to alternate between them. The first is treating every alert as urgent, which turns a fifteen-item list into a sprint and trains everyone to dread the scanner. The second is the rebound: the list is long, most of it seems irrelevant, so the whole thing gets ignored — including the one item that mattered.
Reachability analysis is the step between those two. It is not a way to avoid patching. It is a way to decide what to patch this week versus what to patch on the normal maintenance cycle, and — critically — to be able to explain the difference to someone who asks.
2What Reachability Actually Means
A published vulnerability describes a way to make a piece of code behave badly. For that to matter in your system, three things all have to be true at once. If any one of them is false, the advisory is real and the risk to you is not.
| Condition | What makes it false |
|---|---|
| The vulnerable code is present in what you actually run | It is a build-time or dev-only dependency; the runtime it needs is not installed; the directory is excluded from the image |
| Your code uses the vulnerable feature | The advisory concerns an API, class or mode your application never calls or imports |
| Untrusted input can reach that feature | The only inputs are values your own system produced, or the path is only ever invoked at build time by a developer |
The order matters. Check presence first, because it is the cheapest question and it disposes of whole categories at once. Then feature usage, which is a search. Then input flow, which is the only one that takes real thought.
3Question 1: Is the Vulnerable Code Even in Your Build?
A manifest is a statement of intent, not an inventory of what ships. Several very common arrangements mean a package listed in your project is absent from the thing that runs in production:
- No runtime for it. A Python service whose container image installs no Node runtime cannot execute a JavaScript package, however many of them appear in a
package.jsonused for local asset builds. - Excluded from the image. A
.dockerignoreentry fornode_modules/or a build directory means the package is on the developer's machine and not in the artifact. - Development-only dependency. Test frameworks, linters, type stubs and build tooling are typically absent from a production install — but they still appear in scanner output unless it is told to separate them.
- A transitive dependency of an unused optional extra. Installed because it was declared, never imported by anything.
# What does the running image actually contain?
docker run --rm --entrypoint sh your-image:tag -c "command -v node || echo NO_NODE_RUNTIME"
docker run --rm --entrypoint sh your-image:tag -c "pip list 2>/dev/null | grep -i package-name"
# Is the directory excluded from the build context?
grep -n "node_modules\|/build\|/dist" .dockerignore
# Is it a dev-only dependency?
grep -n -A30 "devDependencies" package.jsonOn a service that carries a front-end asset pipeline alongside a backend application, it is routine for the majority of dependency alerts to concern packages that never reach the production image at all. That is a legitimate reason to deprioritize — and it is a claim you can demonstrate in one command rather than assert.
4Question 2: Do You Use the Vulnerable Feature?
Advisories are usually narrower than their titles. A vulnerability is rarely "this library is unsafe"; it is "this class, called this way, with this option set, behaves incorrectly." The practical work is reading the advisory closely enough to identify the specific API, then searching your codebase for it.
Read the advisory for three things: the affected component or entry point, the configuration required to reach it, and any mitigating condition the advisory itself names. That last one is frequently the whole answer — advisories often state plainly that a particular setting or usage pattern is not affected.
# Wrong question: "do we depend on this library?" -- the scanner
# already answered that. Right question: "do we call the thing
# the advisory is about?"
# e.g. an advisory affecting only a sandboxed-execution class:
git grep -n "SandboxedEnvironment\|from_string"
# e.g. an advisory affecting only one parser mode:
git grep -n "load(\|unsafe_load\|Loader="A zero-result search is a meaningful finding, but only if you searched for the right thing. Record the exact search you ran alongside the conclusion, so that the reasoning can be checked later by someone who doesn't have to take your word for it.
5Question 3: Can Untrusted Input Reach It?
If the code is present and the feature is used, the remaining question is whether anything an attacker controls can get to it. "Untrusted" here means input crossing a boundary you don't control: request bodies and query parameters, uploaded files, webhook payloads, third-party API responses, and message queue contents that originate outside your system.
Two distinctions do most of the work:
- Runtime versus build time. A vulnerability in a tool that only ever runs in CI, on inputs from your own repository, has a very different exposure profile from the same vulnerability in a request-handling path.
- Pre-authentication versus post-authentication. A parsing flaw on a route that requires a valid session is bounded by who has an account. The same flaw on a sign-in form or a public webhook is reachable by anyone.
This is the point at which a component that survived the first two questions usually becomes a real, prioritized item. A form-parsing vulnerability is not interesting in the abstract; it is interesting because the sign-in page accepts form input from the network before any authentication happens.
6Three Worked Examples
The same three questions, applied to three advisory shapes that come up constantly.
A sandbox escape in a template engine
The advisory describes bypassing the library's sandboxed execution mode. Question 2 is decisive and cheap: search for the sandboxed environment class and for the API that compiles templates from strings. If the application only renders templates it ships with and never imports the sandbox, the vulnerable feature is not in use. Not reachable — the sandbox is a feature you have to opt into, and the application never did.
A vulnerability in a JavaScript rendering toolchain
Question 1 settles it before question 2 is worth asking. If the production image installs no Node runtime and the toolchain's directory is listed in .dockerignore, the package cannot execute in the deployed artifact at all. Not reachable, and demonstrable in two commands.
A denial-of-service flaw in a form-parsing library
Present in the runtime image: yes. Feature used: yes — it is what parses submitted forms. Untrusted input: yes, and before authentication, because the sign-in form is a form. Reachable. This is the one that gets fixed, and it is usually a version bump. One genuinely actionable item out of a long list is a normal and good outcome.
7Writing Down What Would Change the Answer
"Not reachable" is a statement about the current build, not a permanent property. The template-engine conclusion above holds exactly as long as nobody imports the sandbox. The toolchain conclusion holds exactly as long as the image installs no JavaScript runtime. Both of those are one ordinary pull request away from being false, and the person who makes that change will not know they invalidated a security assessment.
Every "not reachable" finding should carry the specific, checkable condition that would reverse it — for example: "Reverses if the application begins rendering user-supplied templates, or if a JavaScript runtime is added to the production image." This turns a point-in-time judgement into something a future reader can re-evaluate in a minute, and it is the difference between analysis that ages well and analysis that quietly becomes wrong.
Where the reversal condition maps to something mechanical, enforce it: a test that fails if the sandbox class is imported, or a build check that fails if a runtime appears in the image. A reversal condition nobody watches is a comment.
8What Reachability Analysis Is Not
Three misuses worth naming, because each one turns a good technique into a bad habit.
- It is not a reason never to update. "Not reachable" means "not urgent," not "leave it." Unreachable-but-outdated packages still go on the normal maintenance cycle, because the cost of a routine version bump is near zero and the cost of being wrong is not.
- It is not a substitute for reading the advisory. The entire method depends on correctly identifying which feature is affected. An analysis built on a skimmed summary produces confident, wrong answers.
- It is not something to do from memory. Work from the scanner's actual findings list, not from your own earlier summary of it. Summaries lose items, and a reachability conclusion about a vulnerability you forgot to include is indistinguishable from never having looked.
There is also a scope limit worth being explicit about: assessing reachability from source establishes whether a path exists. It does not prove exploitability, which requires exercising the running system. Where that distinction matters — and in regulated or contractual contexts it usually does — say which one you did.
9Recording the Result So It Survives
The output of this work is worth more than the decision it produced, because the next scan will return most of the same items. A durable record for each advisory contains:
- The package, version, and advisory identifier.
- The specific affected feature, as named by the advisory.
- The answer to each of the three questions, with the command or search that established it.
- The conclusion, and the reversal condition if the conclusion was "not reachable."
- The date, and the commit the assessment was made against.
That last line is the one most often left out and the one that makes the rest trustworthy. An assessment that does not name the commit it was made against cannot be reproduced, and a conclusion nobody can reproduce is a conclusion that has to be redone from scratch.
10Reachability Triage Checklist
For Each Advisory
- The advisory has been read, and the specific affected component identified — not just its title.
- Presence in the deployed artifact confirmed or ruled out against the artifact itself, not the manifest.
- Feature usage established by searching for the affected API, and the search recorded.
- Input flow traced: can anything crossing a trust boundary reach it, and does that require authentication?
- Conclusion recorded with its reversal condition and the commit it was assessed against.
For the Set as a Whole
- Every item on the scanner's findings list has been dispositioned — worked from the list itself, not a summary.
- Reachable items are scheduled now; unreachable ones are on the normal maintenance cycle, not deleted.
- Reversal conditions that can be enforced mechanically have a test or build check.
11Frequently Asked Questions
12Conclusion
A dependency scanner tells you what is present and what has an advisory against it. Three questions turn that into a decision: is the vulnerable code in what you actually run, does your code use the vulnerable feature, and can untrusted input reach it. Most alerts fail the first or second question, which is why a long list frequently contains one item that genuinely needs attention.
The discipline that makes this defensible rather than convenient is recording the reasoning — the search you ran, the command that proved absence, the condition that would reverse the conclusion, and the commit it was all assessed against. Done that way, the analysis is reusable next quarter. Done from memory, it has to be redone, and nobody will.
For prioritizing the items that are reachable, see Vulnerability Prioritization Beyond CVSS. For the pipeline controls that keep dependency review continuous rather than periodic, see CI/CD Security Controls Every Engineering Team Should Have.
Apply This With the Decision Center
This article supports CI/CD Security Scanner — see the product page for how Strata applies these ideas directly.
Visit CI/CD Security Scanner →More From the Knowledge Center
- How Engineering Managers Quantify Application Security Risk
- CI/CD Security Controls Every Engineering Team Should Have
- Repository Security Assessment Checklist
- APK Static Analysis, Explained
- IPA Security Testing, Explained
- Vulnerability Prioritization Beyond CVSS
- How to Secure AI-Generated Code Before You Ship It
- Hardcoded Secrets: Why They Still Reach Production and How Attackers Find Them
- Verifying a Credential Rotation Actually Took Effect
- Sending Sensitive Data to an AI Service: De-identification That Fails Closed
- Where Static Analysis Stops: What Automated Scanning Can and Cannot Prove