Issue #44: security review + threat model¶
- Issue: #44
- Started: 2026-04-28
- Completed: —
Goal¶
Three docs shipped on feat/issue-44:
docs/architecture/threat-model.md— STRIDE-per-surface, asset list, walk-through scenarios. The reference any future security-touching ticket points back at.docs/reference/security-baseline.md— concrete controls list, tier (M1 must-have, M2, M3), and owner-of-control (us / Cloudflare / Auth.js / etc).docs/rfc/0006-security-controls.md— proposes the load-bearing decisions to MASTER (Auth.js hardening, CSP shape, rate-limit topology, audit-log retention, vulnerability disclosure). The RFC numbering sequel — 0001..0004 are taken; the original prompt suggested0008but that referenced a future-state where 0005..0007 already existed; using the actual next number.
No code changes, no schema migrations, no push, no PR. mkdocs build --strict passes. One atomic commit on the branch.
Out of scope¶
- Implementing any control. The deliverables propose; MASTER signs off; tickets land later.
- Authoring ADRs. RFC stage — ADRs follow MASTER's decisions on the open questions.
- Threat-modelling features that don't exist yet at any concrete level (e.g., M3 retailer self-onboarding UC-K — flagged as deferred, not modelled).
- General application-security training material. The deliverables are operational artefacts for THIS architecture (Cloudflare + Postgres + Workers), not a generic OWASP refresher.
- Penetration testing or live exploit walkthroughs. Threat model is theoretical + control-driven.
Approach¶
Foundation ticket → docs only. The pattern is the same as Issue #34: audit current shape → dispatch parallel research → synthesise → wire docs → strict build → atomic commit → stop.
- Read first — issue-44 prompt enumerates the surfaces and assets, reuses the canon from
tech-stack.md,env-vars.md, the architecture set, RFC-0001..0004, ADR-0003, the click-redirect route source, and the use-cases page. - Dispatch parallel STRIDE agents — one agent per major attack surface, each runs full STRIDE (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege) and returns a tight markdown brief: per-element threats, 1–3 mitigations, residual risk. Surfaces:
- (A) Web app request path — Next.js route handlers + server actions (incl. anonymous build cookie per ADR-0003), CSP/headers, edge attack surface.
- (B) Click-redirect endpoint + affiliate-fraud —
/api/go/r/[retailerId]/p/[listingId], click-token replay,IP_HASH_SECRET, S2S postback shape (#17 future). - (C) Auth.js + admin — OAuth callback flow, account-takeover via OAuth merge, session cookie security,
/admin/*authorisation + audit log. - (D) Scraper ingest + email-alert — parser injection / retailer-poisoning / image proxy; price-drop alert dispatch abuse (#14 future); LLM extraction prompt injection (#21 future).
- Synthesise — the four briefs collapse into the threat-model architecture page + the baseline reference list. The baseline rows include tier and owner. The five named scenarios from the prompt get their own walk-through section.
- Verify control names — Cloudflare WAF rate-limit, Cloudflare Bot Management, Cloudflare Turnstile, Auth.js (formerly NextAuth.js) v5+ API surface — names lifted from current docs, not 2024-era blog posts. Spot-check via the agents (which have WebFetch/WebSearch) and against any pages in
node_modules/nextif applicable. - CSP sanity — propose a concrete
default-src 'self'; …directive set with the shadcn nonce-strategy noted (RFC-0004 hasn't shipped yet; the CSP proposal pairs assumptions with the planned shadcn integration). If any directive looks suspect, run it through https://csp-evaluator.withgoogle.com/ before locking it into the RFC. - Wire docs — register the new files in
mkdocs.yml(Architecture:adds threat-model,Reference:adds security-baseline,Proposals (RFC):adds 0005). Updatedocs/architecture/index.md,docs/reference/index.md,docs/rfc/index.mdindexes. - Gate —
mkdocs build --strictclean, no broken refs, no warnings. - Commit + report — one atomic
docs(security): threat model, baseline, controls RFC (#44)on the branch. No push, no PR. Report per the prompt's report shape.
Steps¶
- Step 1: Confirm surface inventory + asset list
- Surfaces: (A) request path + cookies, (B) click-redirect + affiliate, (C) Auth.js + admin, (D) scraper + email-alert.
- Assets: scraped catalog (low secrecy / high availability), user PII (email, hashed pw if any, build state), affiliate revenue data (low risk M1, high M2), Cloudflare account credentials, DB connection string (
DATABASE_URL), IP-hash salt (IP_HASH_SECRET), build-session cookie key (BUILD_SESSION_SECRETper ADR-0003), Auth.js session secret (AUTH_SECRET), OAuth client secrets, Resend API key (#14), Anthropic API key (#21). - Adversaries: generic web attacker, retailer-side bad actor, affiliate-fraud clicker, DDoS / scraping-abuse, account-takeover via OAuth merge, opportunistic credential stuffing, supply-chain compromise of one of the freshness-band deps.
-
Verification: each item maps into one of (A)/(B)/(C)/(D).
-
Step 2: Dispatch four parallel STRIDE agents
- One agent per surface (A/B/C/D). Each prompt: target architecture (Cloudflare Workers + Pages, OpenNext adapter, Postgres on Neon, pg-boss queue, Auth.js v5, Resend, Anthropic, shadcn/ui frontend); STRIDE matrix per element of the surface; 1–3 mitigations per cell; residual risk; explicit references to the current Cloudflare API names and Auth.js v5 API surface (verify via WebFetch/WebSearch); flag anything the threat model surfaces that needs MASTER input.
- Each returns a 200–500-line markdown brief with: STRIDE table, mitigations, names of Cloudflare/Auth.js controls verified against 2026 docs, and a one-paragraph residual-risk summary.
-
Verification: four briefs collected; each names the surface, lists ≥6 STRIDE rows, ≥1 mitigation per row, and has a residual-risk paragraph.
-
Step 3: Walk the five named scenarios
- Aggressive scraping-of-961tech (DDoS / competitive scraping).
- Compromised retailer site (malicious product image / title / link).
- Account takeover via OAuth-merge.
- Affiliate-click spoofing.
- Listing-data poisoning via SQL injection.
- For each: threat statement, attack chain, control mapping (which baseline rows defeat it), residual-risk gap.
-
Verification: each scenario closes with named baseline-row IDs.
-
Step 4: Write
docs/architecture/threat-model.md - Frontmatter:
title,description,status: new, tags[architecture, security, threat-model]. - Sections: Asset register; Adversary register; Surfaces (A/B/C/D) with STRIDE tables; Five scenario walk-throughs; What this doc does not do (no CVEs, no live pentest output, no code-level audit beyond the click-redirect route); Cross-references.
- Diagrams: one mermaid
flowchartshowing surfaces and their adversaries; one mermaidsequenceDiagramfor the OAuth account-takeover scenario. -
Verification: every STRIDE row appears in exactly one surface; every scenario references at least one surface and at least one baseline-row ID.
-
Step 5: Write
docs/reference/security-baseline.md - Frontmatter:
title,description,status: new, tags[reference, security, controls]. - Tabular: control ID (
SB-001..), name, description, tier (M1 must-have / M2 / M3 / on-incident), owner (us / Cloudflare / Auth.js / hosting / Resend), maps to threat-model row(s), status (in place / planned / proposed / decided-deferred). - Top sections: Secrets management; AuthN/AuthZ; HTTP security headers (concrete CSP draft inline); Rate limiting; Input validation (Zod); Audit log; Affiliate-fraud controls; Disclosure /
security.txt; Dependency hygiene (npm audit gating, freshness-band watch fromtech-stack.md). -
Verification: ≥20 control rows; M1 must-haves are flagged; every M1 row has a named owner.
-
Step 6: Write
docs/rfc/0006-security-controls.md - Use
_template.md.status: new. Status fieldDraft — needs MASTER signoff. - Open questions section drives the RFC: Auth.js scope (canonical vs hardened); CSP directive set (with the shadcn nonce-or-hash decision); rate-limit topology (CF WAF only vs +application token-bucket); audit-log retention (90d / 1yr / forever); vulnerability disclosure (security.txt + email vs Bugcrowd vs nothing); Cloudflare paid features acceptance (Bot Management, Turnstile).
- Recommendation column for each open question.
- Cross-references: #19 hosting, #11 Auth.js, #16 admin, #17 affiliate, #43 observability, #40 legal.
-
Verification: each open question has options + recommendation + reasoning.
-
Step 7: Wire docs into nav + indexes
mkdocs.yml:Architecture:addarchitecture/threat-model.mdafterarchitecture/packages.md(or alphabetically — current section order is structural, not alphabetical; place afterpackages.mdso security ends the section).Reference:addreference/security-baseline.mdafterreference/personas.md.Proposals (RFC):addrfc/0006-security-controls.mdafterrfc/0004-frontend-stack.md.
docs/architecture/index.md: add a row for the threat-model page.docs/reference/index.md: add a row for the security-baseline page; remove from "To write next" if listed (it isn't).docs/rfc/index.md: add row 0005.-
Verification: every new file reachable from rendered site; rendered nav order is structural-by-intent (security at section tail).
-
Step 8: Run the gate
mkdocs build --strict. Zero warnings, zero broken refs.- If broken: fix in place, re-run. Never
--no-strict. -
Verification: command exits 0.
-
Step 9: Commit (single atomic)
- Conventional message:
docs(security): threat model, baseline, controls RFC (#44). - Body summarises: 4 STRIDE surfaces, 5 scenarios, ≥20 baseline rows, RFC-0006 with N open questions.
- No
Closes #44. Foundation work — MASTER reviews the RFC before close. -
Verification:
git statusclean;git logshows the new commit at HEAD; no push, no PR. -
Step 10: Report
- Commit SHA + 2–3-sentence summary; top 5 M1 controls + top 5 M2 controls; open questions for MASTER (with options); sources cited.
Risks¶
| Risk | Likelihood | Mitigation |
|---|---|---|
| Subagent cites a Cloudflare control name from a 2024 blog that's been renamed | med | Each agent prompt requires verification via WebFetch on current Cloudflare docs; spot-check during synthesis |
| CSP directive proposed in baseline doesn't compile under shadcn's runtime-injected styles | med | Run draft directive through Google's CSP Evaluator; note the shadcn style-src posture as an explicit open question in the RFC |
| Auth.js claims drift from v5 surface (project renamed from NextAuth.js late 2024) | med | Verify via https://authjs.dev/ in the agent prompt; cite version explicitly |
| Threat model surfaces a control that needs paid CF tier (Bot Management $5–10/mo+) | low–med | Per "Stop and ask if" trigger — flag in RFC open questions, do not commit MASTER to the spend |
| Breach-notification obligation from #40 changes a control from recommended to mandatory | low | The RFC's open question list flags the legal coupling; baseline does not assert "must" until #40 lands |
| Drift between the 5-named-scenarios in the prompt and the four STRIDE surfaces | low | Scenario walk-throughs explicitly reference baseline-row IDs and surface IDs as a cross-check |
gh issue view 44 is unauthenticated on this box, so issue body not read verbatim |
low | Prompt body enumerates scope + acceptance criteria; flag in report as uncertainty |
Tests¶
Foundation ticket — no unit tests. Gates:
mkdocs build --strictclean.- Spot-check one CSP directive set via the Google CSP Evaluator (out-of-band, manual).
Doc updates¶
-
docs/architecture/threat-model.md(new) -
docs/reference/security-baseline.md(new) -
docs/rfc/0006-security-controls.md(new) -
docs/architecture/index.md(register threat-model) -
docs/reference/index.md(register security-baseline) -
docs/rfc/index.md(register 0005) -
mkdocs.yml(add three nav entries) - ADRs — none in this pass; MASTER decides on RFC-0006 first.
Rollback¶
git checkout master on the branch and discard. All work is docs-only on feat/issue-44. No prod side-effects.