Turn a scanner hit or a CVSS score into a real risk decision by proving, in this deployment, that the vulnerable code is reachable, reached by attacker-controlled input, and exploitable given the mitigations actually in place — because "could be exploited" is not "is exposed" and a version match is not a vulnerability. Use whenever a vulnerability needs prioritizing: "we have 4,000 criticals", "is this CVE exploitable here", "should we patch this now", "the scanner flagged", "dependabot opene...
Scanned 9/3/2026
Install to Claude Code
npx -y skills add annatchijova/SKILLS --skill exploitability-triage --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Exploitability Triage?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/annatchijova-exploitability-triage)More formats (shields.io, HTML) on the badges page.
---
name: exploitability-triage
description: Turn a scanner hit or a CVSS score into a real risk decision by proving, in this deployment, that the vulnerable code is reachable, reached by attacker-controlled input, and exploitable given the mitigations actually in place — because "could be exploited" is not "is exposed" and a version match is not a vulnerability. Use whenever a vulnerability needs prioritizing: "we have 4,000 criticals", "is this CVE exploitable here", "should we patch this now", "the scanner flagged", "dependabot opened 40 alerts", "what's our actual risk from Log4Shell/this-CVE", "reachability", "is this exploitable in our config". Sibling of beyond-the-sink (which refuses to trust the scanner's map) and dependency-provenance (which knows what you actually run); ranks by exposure, not by severity field. It does not write the patch or the exploit, and never certifies a vulnerability exploitable — or safe — without stating what it checked.
---
# Exploitability Triage
A CVSS score is a property of a vulnerability in the abstract. Risk is a property of
*your deployment*. The scanner that prints "CRITICAL 9.8" knows the first and nothing
about the second: whether the vulnerable function is ever called, whether an attacker
can steer input into it, whether the exploit's preconditions hold behind your config,
your WAF, your auth. A queue of 4,000 criticals triaged by the number the tool printed
is not triage — it is the tool's ignorance, re-sorted.
The finding is not "we have CVE-X." The finding is "the vulnerable path is reachable,
reached by attacker-controlled input, and its preconditions hold here" — or it is
"we have CVE-X and it is not exposed, here is why." Both are results. The score alone
is neither.
Two symmetric errors this kills:
- **Severity inflation.** Patching the unreachable 9.8 (a vulnerable function that no
code path ever calls, in a dependency loaded but never exercised) while the 5.3 on
the internet-facing authentication path goes to the back of the queue because the
number was smaller. The attacker sorts by exposure; a defender who sorts by CVSS is
optimizing a different problem.
- **Reachability blindness.** Declaring the app vulnerable because the installed
version matches the advisory, without ever checking that the code shipping in that
version even contains — let alone calls — the vulnerable routine. A version match is
a candidate, not a confirmation.
Composes with the library:
- **beyond-the-sink** — the scanner's keyword/version map is a lead, not the territory; confirm reachability in the real call graph
- **dependency-provenance** — "is the vulnerable dependency actually loaded, at that version, with that code" is answered here, not assumed
- **red-team-auditing** — "exploitable — confirmed" is a claim earned by demonstrating reach, never asserted from a score
- **attack-surface-triage** — exposure = reachability × attacker access; that ranking lives there and feeds this
- **honest-degradation** — unproven reachability is a WARN/candidate, never a silent PASS or a confident CRITICAL
---
## Step 0 — Separate the vulnerability from the exposure
Two different objects, and conflating them is the whole disease:
- **The vulnerability** — a property of the code: this function mishandles this input.
Fixed by the vendor, described by the CVE, scored by CVSS. Real regardless of you.
- **The exposure** — a property of *this deployment*: whether that code is present,
reachable, reached by an attacker, and unblocked by your controls. This is what you
are actually triaging, and the CVE says almost nothing about it.
Write both. "CVE-2024-X: deserialization in libfoo ≤2.3 (the vulnerability). We run
2.1; the vulnerable `readObject` path is called only by the admin import feature,
which is authenticated and internal-only (the exposure)." Now you can decide.
---
## Step 1 — Reachability: is the vulnerable code in the call graph at all
The first filter, and the one the scanner cannot do. Does a path exist from any entry
point to the vulnerable function?
- If the vulnerable routine is never called — dead code, an unused feature, a code
path behind a flag that is off — the vulnerability is present and *not reachable*,
and its practical exposure is near zero regardless of CVSS. Say so explicitly.
- Do not confuse "the library is a dependency" with "the vulnerable function runs."
Most libraries ship far more code than any one app exercises (link
`dependency-provenance`: loaded ≠ exercised).
- This is the same discipline as `beyond-the-sink`: the version-match grep is where you
*start*, not where you conclude.
Unproven reachability is not "safe." It is a candidate awaiting evidence — mark it WARN,
not PASS.
---
## Step 2 — Reached: does attacker-controlled input get there
Reachable in principle is not reached in practice. Trace whether an attacker can
actually steer tainted input into the vulnerable path:
- Is the input to the vulnerable function attacker-controlled, or is it a fixed
internal value / a config file only an admin edits?
- What sits between the entry point and the sink — validation, encoding, a type
boundary — that the taint must survive?
- From *where* can the attacker reach the entry point: unauthenticated internet,
authenticated user, internal network, local only? Exposure scales with access.
"Reachable by an internal admin action" and "reachable by an unauthenticated HTTP
request" are the same CVSS and different emergencies.
---
## Step 3 — Exploitable: do the preconditions hold behind your controls
An exploit is a conjunction of conditions. Enumerate the ones the advisory or PoC
requires, and check each against your reality:
- Non-default configuration the exploit needs (a specific flag, a JNDI lookup enabled,
a permissive parser setting) — is it set in yours?
- Mitigations already in the path: a WAF rule, network segmentation, an allow-list, a
runtime protection, memory mitigations. Do not over-credit these (WAF rules are
bypassable — treat as a WARN reducing severity, not a fix), but do not ignore them.
- Platform/version specifics: the exploit needs a heap layout, an OS, a library
version you may not run.
Each precondition that fails to hold *here* lowers exposure — with the reasoning
written down, so a reviewer can challenge it. "Not exploitable because the WAF blocks
it" is a weaker claim than "not exploitable because the vulnerable parser mode is
disabled by our config"; grade accordingly.
---
## Step 4 — Rank by exposure × impact, and state what you did not check
Sort the queue by the product of *demonstrated exposure* (Steps 1–3) and *impact if
exploited* (what the attacker gains — link `attack-surface-triage` and
`assume-breach-modeling` for blast radius), not by CVSS:
- **Confirmed exposed** — reachable, reached, preconditions hold. Patch/mitigate by
impact. These are usually a small fraction of the scanner's "criticals."
- **Present, not exposed** — reachable-but-not-reached, or a precondition fails here.
Record *why*, patch on normal cadence, and re-open if the deployment changes (that
"internal-only" admin feature becomes internet-facing next quarter).
- **Candidate — reachability unproven** — version matches, reach not confirmed. WARN,
not CRITICAL and not dismissed; the honest state is "we have not established
exposure."
Never let a triage output read as a uniform green or a uniform red. Each verdict
carries what was checked and what was assumed, because the next reviewer — and the
auditor after the breach — will ask "exploitable against what evidence?" A confident
"not exploitable" with no stated basis is exactly the claim that ends careers when the
one unchecked precondition turns out to hold.
---
## The one-line test
If your prioritization would not change when the CVSS score were deleted from every
row, you are triaging exposure. If deleting the score leaves you with nothing to sort
by, you were re-sorting the scanner's ignorance — go establish reachability, because
that, not the number, is the finding.
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!