| T1 |
Cross-tenant read/write (Spoofing/Tampering) |
Forged or missing tenant identity in any data call |
TenantContext validated at construction; keys derived only from ctx; store has no raw-key API; adversarial fuzz suite is merge-gated |
Mitigated at primitive layer; API edge closed for GET /me (ADR-0024) — HTTP API's HttpUserPoolAuthorizer verifies the Cognito JWT before the Lambda runs, and the handler trusts only requestContext.authorizer.jwt.claims.sub, nothing else, as identity. Every future route repeats this shape; each one lands verified, not assumed, as it's added |
| T2 |
Repudiation of sensitive reads (Repudiation) |
Support/admin access to user data |
Break-glass admin role is audit-logged (AD control-plane design); impersonation deliberately not built |
Design committed, lands with control plane (M3) |
| T3 |
Credential theft from vault (Information disclosure) |
DB dump / log leakage |
KMS envelope encryption, plaintext never stored or logged, masked API views (****tail) |
Cipher boundary defined (ADR-0006); KMS impl at M4 — open |
| T4 |
Denial of wallet (DoS/economic) |
One user exhausts shared Bedrock budget |
Per-user daily/monthly caps checked pre-invocation under a platform ceiling (tenancy/budget.py) |
Mitigated at primitive layer |
| T5 |
Elevation via config injection (Tampering/Elevation) |
Malicious rubric/profile content steering the model |
Untrusted-content fencing + sanitization; shadow-replay gate before activation (ADR-0009/0015) |
Mitigated at prompt layer; red-team corpus extension tracked (M8) |
| T6 |
Tenant data residue after deletion (Information disclosure) |
Incomplete GDPR delete |
Delete path = crypto-erase of user data key + row purge (ADR-0007); DSR runbook tested before GA |
Open until exercised end-to-end (M9 gate) |
| T7 |
Billing state tampering / replayed webhooks (Spoofing/Tampering) |
Forged or duplicated billing events flipping plans |
Signature-verifier protocol gates entry; per-event-id idempotency in capped history; transitions auditable with source+timestamp (ADR-0017) |
Mitigated at handler layer; live Stripe signature checks land with real integration (M3-live) |
| T8 |
Due-work scan surface used from outside the dispatcher role (Information disclosure/Elevation) |
A user-facing handler calls DueWorkIndex.due_before directly instead of going through the dispatcher |
Deliberately outside TenantRepository/entities.ENTITIES; documented as the one narrow exception (ADR-0018); row shape is a fixed dataclass carrying only user id/work kind/key/next-due/miss-count — structurally cannot carry posting content, credentials, or settings even if misused |
Mitigated by row-shape constraint; open: ApiStack (ADR-0024) has exactly one route today (GET /me) and it does not touch DueWorkIndex — re-verify this line every time a new route is added, don't assume it stays true |
| T9 |
SSRF via a malicious ATS board id (Tampering/Information disclosure) |
A watchlist entry's ats_board_id gets formatted into a request URL; an attacker-controlled value could try to redirect the request internally |
poll.adapters.base.validate_board_id fails closed on anything outside [A-Za-z0-9_-]{1,100} before it ever reaches a URL template; poll.http.get_json additionally refuses any non-https:// scheme regardless — two independent checks, not one |
Mitigated; property-tested indirectly via the adapter test suite's malformed-input cases (tests/test_poll_adapters.py) |
| T10 |
Malformed or adversarial model output silently becoming a fabricated ScoreResult (Tampering/Information disclosure) |
BedrockScorer's prompt asks for a specific JSON shape; a model can return prose, a wrong type, an out-of-range value, or (in principle) attacker-influenced JD text steering the response shape, not just the scoring judgment T5 already covers |
Every field validated before constructing a ScoreResult — coerced only from safe numeric types, range-checked, non-empty rationale required; a hallucinated facet id is dropped as noise, everything else fails closed with MalformedModelResponseError rather than degrading to a guessed score (ADR-0029) |
Mitigated at the parsing boundary; open: no live model has ever produced these failure modes against this code — the test suite covers documented LLM structured-output failure patterns, not an observed adversarial corpus (red-team corpus extension already tracked under T5 for M8) |
| T11 |
Token theft via XSS in the SPA (Information disclosure/Elevation) |
web/'s access/ID tokens live in localStorage (ADR-0030) — any script-injection vulnerability in the app would let an attacker read them and impersonate the user |
The PKCE flow itself has no client secret to steal (ADR-0022's design point); no CSP, output-encoding audit, or dependency-injection review has been done on web/ yet |
Open: this is a real, un-mitigated gap, not a resolved one — a CSP header and an XSS-focused review belong in M8 hardening (roadmap), the same bucket the red-team prompt-injection corpus (T5) is tracked under; do not treat localStorage token storage as safe by default |