Decide whether a CVE in a dependency actually exposes your application before you scramble to patch: is the vulnerable function on a real call path from your code, do the trigger preconditions hold, and can an attacker control the input that reaches it. Use when an advisory, SCA scan, or dependency bot flags a CVE and you must separate a genuine exposure from unreachable noise, or justify why you are or are not affected. Covers pinning the vulnerable symbol, call-path reachability, preconditi...
Scanned 9/3/2026
Install to Claude Code
npx -y skills add UnboundCompute/security-agent-skills --skill adjudicating-dependency-cve-reachability --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Adjudicating Dependency Cve Reachability?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/unboundcompute-adjudicating-dependency-cve-reachability)More formats (shields.io, HTML) on the badges page.
---
name: adjudicating-dependency-cve-reachability
description: >-
Decide whether a CVE in a dependency actually exposes your application before
you scramble to patch: is the vulnerable function on a real call path from your
code, do the trigger preconditions hold, and can an attacker control the input
that reaches it. Use when an advisory, SCA scan, or dependency bot flags a CVE
and you must separate a genuine exposure from unreachable noise, or justify why
you are or are not affected. Covers pinning the vulnerable symbol, call-path
reachability, precondition checks, and taint from your entry points.
license: MIT
---
# Adjudicating dependency-CVE reachability: affected, or just noisy?
A scanner that lists every CVE in your dependency tree is measuring your
`package.json`, not your exposure. Most flagged CVEs are unreachable: you never
call the vulnerable function, or you call it in a way the bug does not trigger, or
no attacker-controlled input gets there. Adjudicating reachability turns a wall of
red into the short list that actually matters, and gives you a defensible reason
for each one you set aside.
## When to use
- An SCA tool, dependency bot, or advisory flagged a CVE in a library you use.
- You are drowning in "critical" dependency alerts and need to triage by exposure.
- You must justify a "not affected" status (for an auditor, a VEX statement, a
customer) with evidence, not a guess.
- You are deciding whether an urgent patch is actually urgent for *you*.
## Scope check
Authorized source only (your own application and the dependencies it ships). If
you can't name the authorization, stop.
## The loop
1. **Pin the vulnerable symbol.** From the advisory and, crucially, the *fix
commit*, identify the exact vulnerable function(s) or sink in the dependency
and the affected version range. "The library is vulnerable" is not actionable;
"`parse_untrusted()` before v2.3 is vulnerable when called with X" is. Reading
the patch tells you precisely what is unsafe and under what condition.
2. **Test call-path reachability.** Does any path from your own entry points reach
that vulnerable function, directly or transitively? If nothing in your code (or
the code you actually invoke) calls it, you are not reachable through the static
graph. Record that, but note the blind spots below before you call it clean.
3. **Check the trigger preconditions.** Reaching the function is not enough. The
CVE usually needs a specific argument shape, a config flag enabled, a feature
in use, or a size or option the safe callers never pass. Confirm whether your
call sites actually satisfy the condition the fix now rejects. Many "reachable"
CVEs die here because you always call the safe overload or pass the safe flag.
4. **Trace attacker control to the trigger.** Reachable plus triggerable still
needs an attacker in the loop. Trace from your untrusted entry points to the
vulnerable argument. If only trusted, developer-supplied constants reach it,
the exposure is theoretical. If untrusted input reaches the dangerous argument
and satisfies the precondition, you have a real, reachable exposure.
5. **Decide and record.** Reachable + triggerable + attacker-controlled =
`confirmed` affected; set severity from real impact in your context, not the
advisory's generic score. Any leg failing = `killed` / not-affected, with the
exact reason (symbol never called / precondition never met / input not
attacker-controlled). Keep the not-affected record; the same CVE will be
re-flagged next scan and you should not re-triage from scratch.
## Blind spots that turn "unreachable" into "unknown"
- **Dynamic dispatch and reflection.** A call made through configuration, a
plugin loader, dependency injection, `getattr`/`eval`, or a virtual/interface
dispatch may not appear as a static edge. "No path" through one of these is
inconclusive; confirm by reading how the call is wired.
- **Transitive and vendored copies.** The vulnerable code may arrive through a
second dependency, a bundled/vendored copy, or a lockfile pin the manifest
hides. Resolve what you actually ship, not what the top-level manifest lists.
- **Build-time vs run-time.** A dev/build-only dependency has a different exposure
than one on the request path. Say which you are judging.
## Worked example (a kill and a confirm)
> **Kill.** CVE in a YAML library: `full_load` deserializes arbitrary objects.
> Pinning the symbol and searching your call sites shows you only ever call
> `safe_load`; `full_load` is never reached from your code. **Killed** /
> not-affected, `kill_reason` = "vulnerable symbol `full_load` not on any call
> path; only `safe_load` is used."
>
> **Confirm.** CVE in an image-decoding library: a crafted file triggers a heap
> overflow in `decode_frame`. Your avatar-upload handler passes user-uploaded
> bytes straight into the library's decode entry, which reaches `decode_frame`,
> and the affected version is the one you ship. Attacker controls the file.
> **Confirmed** affected, `high`, remediation = upgrade past the fixed version or
> gate uploads through a safe re-encoder.
## Rationalizations to reject
- *"It's rated critical, patch it now."* → Critical to someone with a different
call graph. Adjudicate *your* reachability before you triage priority.
- *"We import the library, so we're affected."* → Importing is not calling. The
vulnerable symbol may never be on a path you execute.
- *"It's reachable, so it's exploitable."* → Not without the trigger precondition
and attacker-controlled input. Check both.
- *"No static path, so we're clean."* → Only if the call could not be made through
reflection, config, or a plugin. Confirm the blind spots.
## Executing this in practice
You need to read the dependency's fix to pin the vulnerable symbol and condition,
a call graph that spans your code into the dependency (including transitive and
vendored code) to test reachability, and a way to trace untrusted input to the
trigger argument. A code property graph built over your app plus its resolved
dependencies answers all three; without one, you read the call sites and the
patch by hand. The verdict is yours; the graph only shows the paths.
## Related
- `extracting-nday-from-a-patch` - reading the dependency's fix to pin exactly
what is vulnerable and how it triggers.
- `adjudicating-taint-paths` - tracing attacker control from your entry points to
the trigger argument.
- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - the shape every affected and
not-affected decision takes.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!