Skip to content

Issue #44: security review + threat model

  • Issue: #44
  • Started: 2026-04-28
  • Completed:

Goal

Three docs shipped on feat/issue-44:

  1. docs/architecture/threat-model.md — STRIDE-per-surface, asset list, walk-through scenarios. The reference any future security-touching ticket points back at.
  2. docs/reference/security-baseline.md — concrete controls list, tier (M1 must-have, M2, M3), and owner-of-control (us / Cloudflare / Auth.js / etc).
  3. 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 suggested 0008 but 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.

  1. 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.
  2. 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:
  3. (A) Web app request path — Next.js route handlers + server actions (incl. anonymous build cookie per ADR-0003), CSP/headers, edge attack surface.
  4. (B) Click-redirect endpoint + affiliate-fraud/api/go/r/[retailerId]/p/[listingId], click-token replay, IP_HASH_SECRET, S2S postback shape (#17 future).
  5. (C) Auth.js + admin — OAuth callback flow, account-takeover via OAuth merge, session cookie security, /admin/* authorisation + audit log.
  6. (D) Scraper ingest + email-alert — parser injection / retailer-poisoning / image proxy; price-drop alert dispatch abuse (#14 future); LLM extraction prompt injection (#21 future).
  7. 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.
  8. 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/next if applicable.
  9. 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.
  10. Wire docs — register the new files in mkdocs.yml (Architecture: adds threat-model, Reference: adds security-baseline, Proposals (RFC): adds 0005). Update docs/architecture/index.md, docs/reference/index.md, docs/rfc/index.md indexes.
  11. Gatemkdocs build --strict clean, no broken refs, no warnings.
  12. 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_SECRET per 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 flowchart showing surfaces and their adversaries; one mermaid sequenceDiagram for 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 from tech-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 field Draft — 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: add architecture/threat-model.md after architecture/packages.md (or alphabetically — current section order is structural, not alphabetical; place after packages.md so security ends the section).
    • Reference: add reference/security-baseline.md after reference/personas.md.
    • Proposals (RFC): add rfc/0006-security-controls.md after rfc/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 status clean; git log shows 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 --strict clean.
  • 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.