Skip to content

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 the cve-deferred label; 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:

  1. A sink word in the description — child_process, spawn, exec, eval, argv, stdio, deserializ, interpolat, jq filter, command-safety, stop-parsing, and similar.
  2. 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.