CVE triage: what this library can and cannot guard¶
.github/workflows/cve-watcher.yml polls NVD for MCP-ecosystem CVEs and opens a
tracking issue for some of them. This page is the rule it applies and the
vocabulary every disposition uses. It exists because the watcher's first
48 hours produced ten issues — nine HIGH, one CRITICAL — of which six were
dispositioned out of scope within a day.
That ratio is not a failure of the feed. All ten were genuine MCP-ecosystem CVEs. It is a property of the ecosystem: most disclosed MCP CVEs are server-side authorization defects, and agent-airlock is a contract layer for tool-call arguments. Ten open HIGH/CRITICAL issues on a security library's tracker reads as an unpatched backlog whatever the truth is, so the rule below decides what opens a row.
The seam¶
agent-airlock runs in-process, at the tool-call boundary. It sees the arguments a caller passes to a decorated function, and it can refuse them. It does not sit in an HTTP handler, it does not see requests that never reach a tool, and it cannot observe what another process does after it returns.
Everything below follows from that one sentence.
In scope¶
A defect is in scope when the dangerous thing is carried in an argument the caller supplies:
| Shape | Example from the corpus |
|---|---|
| Command / argument injection into a spawn | CVE-2026-42271 — LiteLLM MCP bridge spawns caller-supplied command/args/env |
| Code injection into an interpreter sink | CVE-2025-59528 — Flowise Function()/eval over a tool argument |
| Delimiter break-out in generated code | CVE-2026-11393 — AgentCore CLI triple-quote neutralisation bypass |
| Path traversal / arbitrary write via a path argument | CVE-2025-68143, CVE-2026-27825 |
| SSRF via a URL argument with a static destination | CVE-2026-26118 — Azure MCP IMDS token theft |
| Secret interpolation into an outbound argument | CVE-2026-32625 — LibreChat ${VAR} in a server URL |
| Metacharacter / escape neutralisation mismatch | CVE-2026-19591 — Codex CLI vs PowerShell --% stop-parsing token |
Out of scope¶
A defect is out of scope when agent-airlock is structurally not in the path. These are not backlog items; they are different layers:
| Shape | Why the tool-call boundary cannot reach it |
|---|---|
| Missing or broken authorization on an HTTP endpoint | The request never calls into a decorated tool. Adding an authz check to someone's route is not something a contract layer can express. CVE-2026-79744, CVE-2026-33032, CVE-2026-23744. |
| Cross-tenant authorization on a data-access endpoint | Same: the check belongs to the service's own identity model. CVE-2026-85178. |
| Incorrect authorization logic inside a router or hub | The bug is in which principal is entitled to route the call, not in the call's arguments. CVE-2026-79746. |
| DNS-rebinding TOCTOU inside another process | The rebind happens between that process's own resolve and its own fetch. An argument check on the caller's side cannot observe the window. CVE-2026-18905, CVE-2026-77822. |
| Framework / CMS access control | No tool-call boundary at all. CVE-2026-84779. |
| Model checkpoint or package compromise on disk | Airlock never sees weights or node_modules, only the calls a model emits. Build-time and load-time problems. |
A CVE can be both: CVE-2026-79748 is a missing-authorization defect
(out of scope) whose primitive is an attacker-supplied stdio spawn config
reaching child_process.spawn (in scope, and already refused by
McpSubprocessArgInjectionGuard). Where the two split, the primitive decides
whether a second-defence regression test is worth adding.
Disposition vocabulary¶
Every cve-response issue is closed with exactly one of these, on one line:
in-scope-and-scheduled— a guard or regression fixture is being written. The issue stays open until it lands.in-scope-and-deferred-until-<DATE>— real, but not now. Apply thecve-deferredlabel; the watcher stops counting it against its back-pressure limit, and the date is a commitment to look again, not a promise to ship.out-of-scope-because-<one clause>— name the structural reason and quote the NVD description so the judgement is checkable by someone who disagrees. Then close as not planned.
Closing eight of ten with a reason is a stronger signal than opening eight speculative guards. Out-of-scope is the expected outcome, not an admission.
What the watcher files¶
classify_shape() in scripts/cve_watcher.py applies the split above as a
triage prior, using two signals from the NVD record:
- A sink word in the description —
child_process,spawn,exec,eval,argv,stdio,deserializ,interpolat,jq filter,command-safety,stop-parsing, and similar. - An argument-shaped CWE — CWE-77, 78, 88, 94, 95, 116, 150, 502, 1336.
Either signal marks the record cve-candidate and opens an issue. Everything
else is listed in the workflow run summary and not filed; it stays in the
watcher's dedup state, so nothing is lost and a human can pull one forward.
The sink scan skips sentences that say a component is not affected. A sink
word inside an exoneration is evidence of safety, not of an argument-shaped
defect. CVE-2026-59971 is why: its only sink match in the entire NVD record
was the word stdio, inside "The default stdio transport is not affected."
Neither of its CWEs is argument-shaped, so that one word opened a CVSS 10.0
tracker row for a CVE whose defect is a missing authentication check — the fake
backlog this page exists to prevent, produced by the mechanism meant to prevent
it. The rule is narrow on purpose: a mitigation is not an exoneration, so
"stdio-only deployments have reduced network reachability" still counts as a
live sentence. Both directions are pinned in
TestASinkWordInsideAnExonerationDoesNotCount.
The prior is not a verdict. A filed issue still needs a human disposition, and
out-of-scope remains a valid answer for something the classifier let through.
This criterion is pinned by tests/test_cve_watcher.py, which replays all ten
CVEs from that first queue with the disposition a human actually gave each one
and asserts the classifier would have opened four issues rather than ten. A
change to the sink list or the CWE set that starts re-admitting the six
server-side authorization CVEs fails there.
Adding a guard once a CVE is in scope¶
See the checklist in tests/cves/README.md,
which also carries the out-of-scope table for CVEs already ruled on.