# Security taxonomy — the coverage contract

audience: AI coding agents first. BLUF-ordered, imperative. Tag [MEASURED]/[INFERRED].
This is the coverage LEDGER: a class with 0 cells or unmeasured recall is a BLIND SPOT, never reported covered (spec §6 no-false-coverage-claim).

## How coverage is anchored
Coverage is EXHAUSTIVE and taxonomy-driven (spec §3 COVERAGE knob), NOT donor-gated. Anchors:
- **OWASP ASVS** (application security verification standard) — the breadth spine.
- **CWE Top-25** — the recognized weakness names.
- **S1–S11** — the fable worked set below, the cells we have ground truth for. **S12** (race conditions / idempotency) and **S13** (business logic / amount-bound) are the first classes beyond the fable set, harvested 2026-06-20 from real payment fixes (trance / platform).

A new class enters the bench (`bench.py`) the moment it has ≥1 git-pinned cell. Recall becomes a RATE only at n≥3 cells/class.

## S1–S13 worked set — MEASURED status (2026-06-17; S12 race + S13 business-logic added 2026-06-20)

| id | class | band that catches it | status | cell(s) / fix SHA |
|----|-------|----------------------|--------|-------------------|
| S1 | auth / session / token lifecycle | band-1 (single-file logic) + band-2 (cross-file 2FA-on-reissue) | [MEASURED] single-file CAUGHT (reset-token reuse, 2/3); cross-file Shape-B MISS single-file (0/3), CAUGHT once resolved (3/3) | `S1__password.ts` 59ec337; `refresh.ts`/`session.ts` ead618d |
| S2 | RBAC / authorization | band-1 (single-file Shape-A escalation) | [MEASURED] recall CAUGHT 3/3 cells (RED [critical] each, hand-judged; team THIN/variance-prone 2/3); GREEN canonical-clean on all 3 — invite+team residuals are TRUE POSITIVES (canonical-clean ≠ globally-clean), not noise; only precision thread = severity calibration (team TOCTOU over-rated, NOT task #17); payments-prompt AuthZ generalizes to non-payments RBAC | roles 1b19ed1, invite 2a70b38, team 43d8e10 |
| S3 | tenant isolation | band-1 (single-file Shape-A IDOR) | [MEASURED] CAUGHT 3/3 cells (RED 3/3 rolls each, clean GREEN) — read + write + self-documented IDOR on NON-payments files; first class measured as a RATE (n=3), not a point estimate | calendar 55fcaf4, sessions 7ec19d6, report-sched 5715f6d |
| S4 | SQL injection | band-1 | [MEASURED 2026-06-18] CAUGHT 3/3 + discriminator PASS (n=1 cell, point-estimate) | `S4-kb-spaceids-sqli` cell, fix `9b0c485` (sql.raw ARRAY interpolation → inArray) |
| S5 | output encoding / XSS | band-1 | [MEASURED] CAUGHT 3/3 | `S5__render.ts` a15deb0 |
| S6 | SSRF | band-1 | [MEASURED 2026-06-18] CAUGHT 3/3 + discriminator PASS (n=1 cell, point-estimate) | `S6-jira-baseurl-ssrf` cell, fix `bec8bc8` (tenant baseUrl unguarded fetch → assertSafeOutboundUrl) |
| S7 | webhook signature verification | band-1 | [MEASURED] CAUGHT 3/3 (fail-open on falsy secret) | `S7__whatsapp.ts` 5a2867f |
| S8 | cross-tenant IDOR (DB query path) | band-1 | [MEASURED] CAUGHT 2/3 | `S8__attachments.ts` 30ebf2a |
| S9 | payments / complete-mediation (self-deal) | band-3 oracle (C02/C09) + band-1 | [MEASURED] oracle catches C02 (no LLM config did); **held-out #13: the oracle is DOMAIN-BLIND → ONE generalization boundary = the field-name lexicon + `key:value` sink form. Inside it the asymmetry logic fires end-to-end on novel input (H1 FIRE / safe SILENT = integration+discrimination); outside it = SILENT MISS (natural off-payments names; ES6-shorthand sink prop). NOT gated on self-deal structure** | multideal C02/C09; held-out bounty cells |
| S10 | OAuth / auth-code replay | band-1 | [MEASURED] CAUGHT 2/3 (non-atomic mark-used) | `S10__token.ts` 960bb60 |
| S11 | dependencies / CVE + security headers | band-3 deterministic (`pnpm audit` + header/config check) | [MEASURED 2026-06-17] BUILT — `deps_audit.py` (real CVE-2020-7661/url-regex RED, clean GREEN) + `headers_scan.py` (OWASP required-set RED/GREEN), deterministic, no LLM | `detectors/{deps,headers}/` + `cells/`; `tests/test_s11_band3.py` |
| S12 | race conditions / idempotency (TOCTOU double-spend) | band-1 | [MEASURED 2026-06-20] CAUGHT 3/3 right-reason (critical) + discriminator PASS (n=1 cell, point-estimate); GREEN surfaces a real orphaned-`capturing`-state TP (canonical-clean ≠ globally-clean) | `S12-paypal-capture-double-spend` cell, fix `d38a6b17` (check-then-capture on `order.status` → atomic `UPDATE payments SET status='capturing' WHERE status='pending' RETURNING` claim before `capturePayment`) |
| S13 | business logic / amount-bound (non-positive fixed discount) | band-1 single-file FLAKY → band-2 | [MEASURED 2026-06-20] band-1 single-file FLAKY: NEUTRAL-path k=3-union CAUGHT but **1/3 per-roll** (k=1 ≈33%); the leaky-path 3/3 was attention-primed (defect word "nonpositive" in the cell-dir path), exposed by a neutral-path control → routes to band-2. GREEN discriminator PASS (recall=0, run at the harder leaky path). n=1 point-estimate. Direct money-loss end (over-refund) = recorded band-2 follow-on | `S13-promo-fixed-discount-nonpositive` cell, fix `87fd985` (`assertPromoShape` 'fixed' branch accepted `valueAmount ≤ 0` while percentage/bogo bound theirs → add `if (valueAmount <= 0n) throw`) |

## Coverage summary [MEASURED 2026-06-17]
- **11 of 13 classes MEASURED.** 10 are recall-CAUGHT clean by the generalist band on single-file canonicals (S1,S2,S3,S4,S5,S6,S7,S8,S10,S12). **S12 (race/idempotency)** added 2026-06-20 [MEASURED]: band-1 CAUGHT 3/3 right-reason (critical) + discriminator PASS, n=1 point-estimate (rate-promotion corpus-blocked on CLEAN cells; candidate platform `c3b9c35` `FOR UPDATE`). **S13 (business-logic / amount-bound)** added 2026-06-20 [MEASURED] is band-1 **FLAKY single-file** — NEUTRAL-path k=3-union CAUGHT but 1/3 per-roll (k=1 ≈33%); the leaky-path 3/3 was attention-primed, exposed by a neutral-path control → **routes to band-2**; discriminator PASS, n=1. **S13's neutral-path control flags a methodology follow-up: QUANTIFY S12/S4/S6 at neutral paths (measure, don't assume wrong). Priming scales with path-PRECISION — S13's dir encodes the FIX CONDITION ("nonpositive"), so it is likely the MOST inflated; S12/S4/S6 dir names are class/vuln-type hints ("double-spend"/"sqli"/"ssrf"), not the specific finding, so likely LESS inflated. The real fix is path-blinding the single-file bundle in `gate.py` (`else: module_path = target`).** S13 is the amount-bound END of business_logic; the direct money-loss end (over-refund `refund > amountPaid`) is cross-file → a recorded band-2 follow-on (platform `823ddea` cap-poison; multideal `8641e9267`/`d09dd6e1a` `REFUND_EXCEEDS_PAID`), NOT yet built. See `docs/validation/2026-06-20-S12-race-condition-band1.md`, `docs/validation/2026-06-20-S13-business-logic-band1.md`.
- **S2 + S3 are measured as RATES (n=3 cells each)**, not n=1 point estimates. S3 = clean GREEN throughout. S2 = recall CLOSED 3/3 (team THIN/variance-prone); GREEN is canonical-clean on all 3 — the invite stale-JWT + team TOCTOU residuals are TRUE POSITIVES (git-fix `safe.ts` is canonical-clean, NOT globally-clean; the gate correctly surfaces distinct real issues, more on subtler cells), NOT precision noise. The only precision thread is severity calibration (team TOCTOU rated `critical` is over-rated) — this is NOT task #17 (paraphrase-dedup, a different phenomenon). Both confirm the payments-FRAMED prompt's AuthZ/IDOR passes generalize to NON-payments files. See `docs/validation/2026-06-17-S2-rbac-band1.md`, `docs/validation/2026-06-17-S3-tenant-isolation-band1.md`.
- **Cross-file:** Shape-A IDOR is FLAGGED by band-1 (recall OK); Shape-B (imported insecure default) is a band-1 single-file MISS whose DETECTION is solved once the symbol is resolved into scope (two-file test 3/3). band-2 DELIVERY is now SHIPPED [MEASURED 2026-06-17]: `gate.py` resolves first-party imports and inlines their BODIES into the target's review as ONE bundle (deps-as-context, not separate scans) → the shipped gate caught the canonical 3/3 [critical] on `S1-xfile-2fa-on-refresh` (`docs/validation/2026-06-17-band2-delivery-shipped.md`). Remaining fast-follow (#36b): barrel/2-hop re-export resolver + tsconfig `paths`/workspaces auto-alias for real monorepos. Scope honesty: n=1 cell, cross-file PRECISION unmeasured beyond it.
- **BLIND SPOTS (no cells / unmeasured recall):** NONE in the named LLM set — S2 CLOSED 2026-06-17 (3 cells, recall 3/3) and S3 CLOSED 2026-06-17 (3 cells). S11 is NOT an LLM blind spot: structural band-3 (`pnpm audit` + headers), BUILT 2026-06-17 (`deps_audit.py` + `headers_scan.py`, deterministic RED/GREEN, `tests/test_s11_band3.py`) — not an LLM miss. **"No blind spots" = every class has a detector + RED/GREEN cell; it does NOT mean 11/11 recall-validated AT RATE.** Recall is a RATE (n≥3) only for S2 + S3; the rest are n=1 point estimates (S1/S4/S5/S6/S7/S8/S10) or oracle (S9) — S4/S6 are now real RED/GREEN cells (discriminator-PASS), no longer pilots. S11 is DETERMINISTIC but each leg is a single cell (deps = 1 real-CVE captured-audit cell; headers = 1 synthetic config cell), and the headers detector has a dominant precision limit — it must be AIMED at the header-config file (it flags all 6 on any non-header file). So S11 = coverage-exists + deterministic-correct on its cell, NOT rate-validated recall. S2 GREEN is canonical-clean on all 3 cells; its invite+team residuals are TRUE POSITIVES (canonical-clean ≠ globally-clean), not noise — the only precision thread is severity calibration (team TOCTOU over-rated), NOT task #17.

## Oracle (S9 self-deal) generalization boundary [MEASURED 2026-06-17, #13]
The deterministic oracle is the ONLY leg that catches the founding C02 self-deal, so its boundary IS the gate's
deterministic self-deal coverage. Held-out test (synthetic 3rd domain, bug-bounty grant disbursement). The headline
is ONE axis, not two — do not report a "domain win" and a "lexicon loss" as separate findings:
- **DOMAIN-BLIND by construction [MEASURED].** The oracle reads field-name tokens + AST, never a "domain" → it has a
  SINGLE generalization boundary: the field-name lexicon + the `key:value` sink form. "Domain" is a cell label, not
  a detector input.
- **Inside the boundary — integration + discrimination [MEASURED].** A lexicon-compliant self-deal in an unseen
  domain FIRES (`INTER-SINK GAP`) and its safe twin is SILENT → the FULL asymmetry pipeline engages end-to-end on
  novel input AND discriminates. (First authoring attempt missed via shorthand → the FIRE is a real confirmation,
  not a tautology.) This is NOT "domain generalization" — lexicon is the only variable that moves catch→miss.
- **The boundary — lexicon + property-form [MEASURED].** Natural off-payments names (`approverId`/`recipientId`/
  `award`/`kind`) = SILENT MISS; ES6-shorthand beneficiary prop = SILENT MISS. A SILENT oracle here is a possible
  FALSE CLEAN with NO guard (unlike unresolved-imports, which IS guarded). Corrects the oracle header's "no hardcoded
  sink NAMES" claim: name-independent on CALLEE/PREDICATE (rename-GATE proven), NOT on FIELD names.
- Recommended fix direction (separate task, brainstorm→plan): a NON-SILENT "value-sink-shaped call with off-lexicon
  fields — UNRELIABLE" notice (cannot overclaim) over an ever-growing field regex (whack-a-mole). Not yet built.
- Evidence: `docs/validation/heldout-oracle-generalization/2026-06-17-heldout-oracle-generalization.md` + `cells/`.

## Pointers (do not re-inline)
- Detection registry — every bug NOT 1-pass-caught + the Phase-1 (99% single-run) / Phase-2 (deferred dice-rolling) strategy; reason about as a batch: `docs/known-hard-classes.md`
- Single-pass recall campaign (49 P0/P1 feeder): `docs/validation/2026-06-18-recall-campaign-49.md`
- Recall evidence: `docs/validation/2026-06-17-recall-spike-S1-S5-S7-S8-S10.md`
- Cross-file evidence: `docs/validation/2026-06-17-band2-crossfile-spike.md`
- Coverage/extraction model: `docs/specs/2026-06-17-security-gate-design.md` §3, `docs/TOOLBANK_DECISION.md`
