Skip to content

Threat model

What this answers: what's worth defending, who'd attack, where the attack surfaces are, and what defences match each. The companion to Reference → Security baseline (concrete controls list) and RFC-0006 (decisions still needed from MASTER).

This page describes the threat model for the target architecture (RFC-0001 leading recommendation: Cloudflare Pages + Workers via the @opennextjs/cloudflare adapter; RFC-0002 leading: pg-boss; RFC-0004 leading: shadcn/ui). It is not generic OWASP advice — it is for THIS stack on Cloudflare with Postgres on Neon and a future Auth.js v5 layer (#11).

What this doc does NOT do: live exploit walkthroughs, code-level audits beyond the /api/go/* route, dependency-CVE tracking. Those belong to a future security-review cadence (#43 observability + alerting).


Asset register

What's worth defending, ranked by impact-on-loss:

Asset Where it lives Confidentiality Integrity Availability Notes
Cloudflare account credentials MASTER Critical Critical Critical Compromise = total system compromise. 2FA mandatory.
GitHub account (owns OAuth apps + repo) MASTER Critical Critical High Same as above for the OAuth side. 2FA mandatory.
Postgres DATABASE_URL (Neon) Cloudflare Workers Secrets Critical Critical High Read access = exfil all user data; write access = data poisoning.
AUTH_SECRET (Auth.js JWE signing) Workers Secrets (M2) Critical Critical n/a Compromise = forge any session. Rotation invalidates all sessions (the brute revocation lever).
BUILD_SESSION_SECRET (ADR-0003) Workers Secrets (M2) High High n/a Compromise = forge anonymous build cookies. Lower blast radius than AUTH_SECRET.
IP_HASH_SECRET (click salt) Workers Secrets Medium n/a n/a Currently defaults to literal dev-secret-rotate-in-prod (env-vars.md § IP_HASH_SECRET). Compromise → IPv4-space brute-force reverses any ipHash. Mandatory rotation pre-prod.
OAuth client secrets (Google, GitHub) Workers Secrets (M2) High High n/a Compromise lets attacker impersonate 961tech to the provider.
RESEND_API_KEY (M2) Workers Secrets High n/a High Compromise = email send abuse from our domain → reputation damage.
ANTHROPIC_API_KEY (M2) Workers Secrets Medium n/a High Compromise = burn quota / dollars; no PII risk.
User PII (User.email, build state, subscriptions) Postgres (M2) High High Medium Legal exposure if leaked (#40 data-breach obligations).
Affiliate revenue data (Conversion, M2 #17) Postgres (M2) Medium High Medium Integrity attacks (click spoofing) directly steal money once CPC payouts attach.
Scraped catalog (Listing, ListingPrice, Product) Postgres Low High High The moat. Public anyway, but integrity attacks (price poisoning) embarrass us and break the value prop. High availability target.
audit_log (M2) Postgres Medium Critical Medium Repudiation defence — must be append-only and tamper-evident.
Cloudflare Pages app source Public on GitHub n/a High n/a Public repo. Integrity guaranteed by Git + signed commits if we ever enable them.

Anonymous-build state is explicitly low-impact (ADR-0003) — losing a Visitor's PC build is annoying, not damaging. The cookie-claim transaction matters more for product correctness than for security.


Adversary register

Adversary Capability Motivation Likelihood Realised threat
Generic web attacker Automated scanning, OWASP-top-10 fuzz, opportunistic Take whatever's exposed High Probes, credential stuffing if a password layer exists, SQLi (closed by Prisma), XSS attempts.
Competitive scraper Scripted crawls of our catalog Steal the moat (the catalog) High Aggressive scraping → DDoS-shaped traffic + data exfil of the public catalog (low-secrecy → primary impact is bandwidth cost).
Affiliate-fraud clicker Bot farms generating clicks Inflate metrics or skim CPC payouts (when #17 lands) Medium Spoofed click traffic → corrupt analytics today, theft tomorrow.
Account-takeover attacker Email reconnaissance + OAuth abuse Hijack a user's account Medium-Low Email-collision merge attack (S2 in surface C), session-cookie exfil via XSS.
Compromised retailer Full control of a retailer site we scrape Various: phishing reputation laundering, price poisoning, SSRF us into our own infra Low Malicious image/title/link, price-poison sales, SSRF the M2 image-fetch into Neon's VPC.
Supply-chain attack Compromised npm dep Code execution in Workers Low (but freshness-band ↑) Five recently-major deps (tech-stack.md § Cross-cutting risks) — single compromise cascades.
Cloud / OAuth-provider compromise Out-of-our-control adversary at Google/GitHub/Cloudflare/Neon/Resend Whatever they want Very low Mint valid sessions, exfil DB, etc. Mitigated only by least-privilege scopes + rotation playbook.
Insider Hypothetical co-admin (M2+) Dispute, malice, accident Low (single-operator at M1) Unaudited admin actions; audit_log tamper.
Lebanese-network adversary ISP-level interception Surveillance, censorship Low Mitigated by HTTPS-only + HSTS at edge. Out of scope for application controls.

Surface map

flowchart LR
    subgraph A["Surface A — Web app request path"]
      A1["Route handlers + server actions"]
      A2["Anonymous build_session cookie"]
      A3["CSP + headers"]
      A4["/api/health"]
    end
    subgraph B["Surface B — Click redirect + affiliate"]
      B1["/api/go/r/X/p/Y"]
      B2["Click tokens (#17 future)"]
      B3["IP_HASH_SECRET"]
    end
    subgraph C["Surface C — Auth.js + admin"]
      C1["OAuth callback"]
      C2["Account merge"]
      C3["/admin/* (#16)"]
      C4["audit_log"]
    end
    subgraph D["Surface D — Scraper + alert + LLM"]
      D1["undici fetch + cheerio"]
      D2["Image fetcher (M2 R2)"]
      D3["Email alerts (Resend)"]
      D4["LLM extraction (Anthropic)"]
    end
    GW["Generic web attacker"] --> A
    SC["Competitive scraper"] --> A
    SC --> D1
    AF["Affiliate-fraud clicker"] --> B
    AT["Account-takeover attacker"] --> C
    CR["Compromised retailer"] --> D
    OP["OAuth-provider compromise"] --> C

Each surface gets its own STRIDE table below. Mitigations roll up into Reference → Security baseline — every cell here cites the baseline-row IDs (SB-NNN) that close it.


Surface A — Web app request path

Scope. Every Next.js route handler other than /api/go/*, every server action (incl. the future /api/build/save per ADR-0003), the anonymous build_session cookie, HTTP security headers + CSP, the edge surface visible via Cloudflare Workers, /api/health.

# STRIDE Threat Mitigation refs
A-S1 Spoofing Forged build_session cookie if BUILD_SESSION_SECRET weak / leaked SB-001, SB-013
A-S2 Spoofing Pre-claim cookie planted on shared browser; victim signs in and inherits attacker's items SB-013; accepted residual (low-value asset)
A-S3 Spoofing Auth.js session cookie forgery via weak AUTH_SECRET SB-001, SB-014 (M2)
A-T1 Tampering Server-action argument tampering — caller bypasses TS types SB-006 (Zod on every action)
A-T2 Tampering .bind() server-action args are not encrypted by Next 16 (Next.js security guide) — caller controls them SB-006, SB-007 (closure-over-bind convention)
A-T3 Tampering build_session cookie bit-flip / re-sign with leaked key SB-013, SB-002 (key rotation)
A-R1 Repudiation No correlation ID on requests; can't reconstruct what a session did SB-019 (request-id in middleware)
A-R2 Repudiation No audit row for "who claimed which anon build into which account" SB-018 (M2)
A-I1 Info disclosure /api/health leaks git SHA, DB state, queue depth → fingerprint SB-008 (minimal health)
A-I2 Info disclosure RSC payload leaks server-only data via Client Component prop drilling — explicitly flagged in the Next.js security guide SB-009 (Data Access Layer pattern), SB-010 (taint APIs)
A-I3 Info disclosure Verbose dev-mode error pages leaking stack traces SB-011 (prod-mode CI gate)
A-I4 Info disclosure Permissive CSP lets a future XSS exfiltrate cookies SB-003 (CSP), SB-004 (security headers)
A-D1 DoS Server-action flood (POST same action 10 k req/min) → Worker CPU + Neon pool burn SB-016 (CF Rate Limiting Rules), SB-017 (in-app token bucket)
A-D2 DoS Oversized build_session cookie stuffing SB-013 (4 KB cap, server-side reject)
A-D3 DoS Hyperdrive connection-pool exhaustion via unauthenticated POSTs SB-016, SB-005 (Hyperdrive bounded fan-in)
A-E1 EoP Server-action authorisation regression — exported "use server" callable as anonymous public SB-007 (in-action authz; never trust UI)
A-E2 EoP TOCTOU between cookie read and account write during claim SB-013 (single-transaction claim)
A-E3 EoP Missing authz check inside server action (UI-gated only) SB-007

Specifics worth landing in code-review checklists:

  1. CSP shape. The starting directive set is in SB-003. The shadcn/Radix style-src posture is an open question for RFC-0006 — Radix @floating-ui injects inline style="..." for Popover / Tooltip / Dialog positioning, which forces either 'unsafe-inline' for styles or a per-render style nonce. script-src stays nonce-locked regardless; XSS-via-style is bounded compared to XSS-via-script.
  2. Headers delivery on Cloudflare Pages. The _headers file applies only to static assets, not to Worker-rendered responses (Cloudflare Pages headers docs). With @opennextjs/cloudflare serving every dynamic route via the Worker, _headers is effectively useless for dynamic responses. Set headers in next.config.ts headers() config.
  3. Server-action defences (Next 16). Built-in: action-ID hashing, closure-arg encryption via NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, Origin-vs-Host CSRF check. Not built-in: argument validation (always Zod), in-action authorisation (always auth() check at top of body).
  4. /api/health minimisation. Return { "status": "ok" } and HTTP 200, nothing else. If a deeper liveness check is needed, expose /api/health/deep behind a X-Health-Token header secret or a Cloudflare Access policy.

Surface B — Click redirect + affiliate

Scope. GET /api/go/r/[retailerId]/p/[listingId]/route.ts (source), the Click table, click-token replay surface, the IP_HASH_SECRET salt, and the future S2S postback flow (#17).

# STRIDE Threat Mitigation refs
B-S1 Spoofing Forged X-Forwarded-For to hide source. Current code reads x-forwarded-for; on Cloudflare Workers the trustworthy header is CF-Connecting-IP (CF docs). SB-021 (switch to CF-Connecting-IP)
B-S2 Spoofing Click-token guessing ahead of #17 postback SB-022 (single-use redemption + short TTL)
B-S3 Spoofing Bot/headless click farms inflating retailer counts SB-016 (CF Rate Limiting Rules), SB-026 (Bot Fight Mode), SB-027 (Turnstile if abuse observed)
B-T1 Tampering Open-redirect (future state). Once admin tooling (#16) lets a non-trusted actor write Listing.url, new URL(listing.url) becomes a phishing surface laundered through 961tech.com. SB-023 (URL host allow-list at write and read), SB-024 (https: scheme enforcement)
B-T2 Tampering source querystring written verbatim → log injection / dashboard XSS if a future admin page renders it raw SB-025 (source enum allow-list)
B-T3 Tampering URL-param pollution on listing.url if logic ever moves from searchParams.set to append/concat SB-024 (defensive build)
B-R1 Repudiation Click written before redirect succeeds → counted clicks that never reached retailer; affiliate reconciliation under-counts SB-028 (two-phase click record: initiatedlanded)
B-R2 Repudiation No append-only audit on Click row tampering SB-018 (audit_log), SB-029 (Postgres role separation)
B-I1 Info disclosure IP_HASH_SECRET default literal 'dev-secret-rotate-in-prod' lets anyone with source access reverse ipHash over IPv4 in minutes SB-001 (Workers Secrets), SB-002 (32-byte random + rotation) — mandatory pre-prod
B-I2 Info disclosure Raw Referer logging leaks originating page query strings SB-030 (strip Referer query before log)
B-D1 DoS Volumetric click flood → DB write saturation SB-016, SB-017, SB-005 (Hyperdrive)
B-D2 DoS 302-redirect amplification cannon at retailers (we become free traffic at them) SB-016
B-D3 DoS Click table storage growth SB-031 (M3 retention policy)
B-E1 EoP Tenant escape via retailerId/listingId mismatch — already checked in code, regression risk SB-032 (regression test pinned)
B-E2 EoP If #17 postback runs in a broad-write DB role, retailer compromise → direct Conversion write SB-029 (role separation per worker)

Concrete bug in current code: req.headers.get('x-forwarded-for') is wrong for the leading-hosting target. On Cloudflare Workers, XFF is whatever the client sent — clients can spoof it freely. The trustworthy header is CF-Connecting-IP (or True-Client-IP if Enterprise). SB-021 tracks the fix; this is independent of #17 and worth its own issue.


Surface C — Auth.js + admin

Scope. OAuth callback flow, email-collision / account-merge logic, session cookie security, /admin/* authorisation gate (#16), audit_log, cookie-claim transaction. Auth.js v5 (the renamed-from-NextAuth.js project) is M2 work (#11).

# STRIDE Threat Mitigation refs
C-S1 Spoofing OAuth callback CSRF / state-fixation Auth.js v5 default checks: ["pkce", "state"] (authjs.dev/reference/core/providers); never override to ["none"]
C-S2 Spoofing Email-collision account takeover — attacker pre-registers GitHub OAuth using victim's email; victim signs in via Google with same email; if allowDangerousEmailAccountLinking: true on either provider, accounts auto-merge → attacker hijacks victim. Auth.js v5 default is off; misconfiguration is the failure mode. SB-040 (allowDangerousEmailAccountLinking: false explicit), SB-041 (callbacks.signIn rejects unverified emails)
C-S3 Spoofing OAuth provider misconfiguration: redirect_uri loose-match, stale dev URI SB-042 (exact-match URIs in provider consoles)
C-T1 Tampering Cookie-claim hijack: attacker steals build_session, signs in with own OAuth, transfers victim's build into attacker's account Accepted residual (low-value asset per ADR-0003); SB-013 (cookie hardening)
C-T2 Tampering JWT session token tampering Auth.js v5 default JWE encryption with AUTH_SECRET; reduces to secret hygiene (SB-001, SB-002)
C-T3 Tampering audit_log tampering by compromised admin SB-029 (DB role separation), SB-018 (append-only by convention; row-level security at M2)
C-R1 Repudiation Admin denies destructive action SB-018 (audit_log inside same Tx as the mutation)
C-R2 Repudiation Sign-in event lacks IP / UA / provider SB-043 (wire events.signInaudit_log)
C-I1 Info disclosure Session cookie XSS exfil via non-HttpOnly Auth.js v5 default HttpOnly
C-I2 Info disclosure Plaintext-HTTP cookie on misconfigured preview deploy Auth.js v5 default __Secure- prefix in production; CF Pages forces HTTPS
C-I3 Info disclosure OAuth scope creep SB-044 (least-privilege scopes: openid email profile / read:user user:email)
C-I4 Info disclosure Verbose error pages on OAuth failure SB-045 (custom pages.error)
C-D1 DoS OAuth callback flood SB-016 (CF Rate Limiting on /api/auth/*)
C-D2 DoS /admin/* enumeration / probe traffic SB-016, SB-046 (require auth before any work)
C-D3 DoS audit_log unbounded growth SB-031 (M3 retention)
C-E1 EoP Admin authz bypass — middleware-only gate evaded by Cloudflare Workers + Next.js 16 RSC quirks SB-046 (defence-in-depth: middleware and auth() check inside every admin server component / action)
C-E2 EoP Env-var allow-list drift (ADMIN_EMAILS stale post-rotation) SB-047 (M2: migrate to User.role DB column)
C-E3 EoP OAuth provider compromise Out-of-our-control; SB-044 (least scope), SB-048 (rotation playbook)
C-E4 EoP Session fixation Auth.js v5 default issues new JWT on sign-in; cookie overwritten
C-E5 EoP Brute-force / credential stuffing N/A at M1 (OAuth-only). Reappears if email-link / passkey added under #11.

Surface D — Scraper + alert + LLM

Scope. undici fetch + cheerio parse, matcher pipeline, Listing upsert + ListingPrice insert, price-drop detection, image fetcher (M2 → R2), email alerts via Resend (#14), LLM extraction via Anthropic (#21).

Trust boundary. The retailer site is outside the trust boundary, even though we initiate the connection. We pull untrusted bytes into a privileged worker process.

# STRIDE Threat Mitigation refs
D-S1 Spoofing DNS hijack / MitM on retailer hostname SB-050 (HTTPS-only, no downgrade)
D-S2 Spoofing Inbound email forging "961tech price drop" From: SB-051 (DMARC p=quarantine on sending domain)
D-S3 Spoofing Subscription created with someone else's email SB-052 (DIY double-opt-in — Resend has no managed flow per resend.com/docs)
D-T1 Tampering Compromised retailer ships <script> / javascript: URL / SVG-with-JS in product title SB-053 (lint-ban dangerouslySetInnerHTML on retailer-derived strings; React JSX auto-escapes by default)
D-T2 Tampering Tracking-pixel imageUrl leaks egress IP + scrape cadence SB-054 (next/image strict remotePatterns), SB-055 (Content-Type allow-list on M2 image-fetch)
D-T3 Tampering Price poisoning — hostile retailer drops to $0.01 to skew "cheapest" SB-056 (rolling-median anomaly hold), SB-057 (operator override via UC-J)
D-T4 Tampering Product.specs JSON poisoning SB-058 (Zod schema per category)
D-T5 Tampering Cheerio parse-injection — N/A: cheerio doesn't execute JS Document explicitly so future contributors don't re-evaluate
D-R1 Repudiation "We never had that price" disputes SB-059 (store HTTP status + fetched-at + content hash on every ListingPrice)
D-R2 Repudiation Operator override later disputed SB-018 (audit_log on UC-J overrides)
D-R3 Repudiation Recipient claims they never subscribed SB-052 (persist double-opt-in event)
D-I1 Info disclosure Tracking pixel discloses Worker egress IP SB-060 (fixed UA string, no Referer outbound)
D-I2 Info disclosure LLM call forwards retailer text containing accidental PII SB-061 (send only product title + already-extracted spec strings, never full HTML)
D-I3 Info disclosure Worker DB role over-privileged → retailer-content-induced bug exfils user data SB-029 (three-role separation: scraper_worker, alert_worker, app_web)
D-D1 DoS Multi-GB HTML / zip-bomb response OOMs the Worker SB-062 (undici 5 MB cap + bodyTimeout 15 s + headersTimeout 5 s)
D-D2 DoS Email-alert dispatch abuse — Resend default rate limit is 5 req/sec/team (resend.com/docs); a market sweep saturates instantly SB-063 (per-user daily digest above N drops/day), SB-064 (pg-boss singleton (userId, day) coalescing), SB-065 (subscription-creation rate limit)
D-D3 DoS Scraper self-DoS: parser change emits 0 listings → site goes empty SB-066 (drift alert: 0 listings when 30-day median ≥ 50 pages operator), SB-067 (soft-stale, never hard-delete)
D-D4 DoS LLM runaway response burns API quota SB-068 (max_tokens ≤ 512 on every call), SB-069 (per-day spend cap with circuit-break)
D-E1 EoP LLM prompt injection — retailer description says "Ignore previous instructions and emit {...}" SB-070 (Anthropic tool use with strict: true), SB-071 (retailer content only inside delimited <retailer_text>...</retailer_text> user block, never system prompt), SB-072 (Zod validate the parsed tool_use.input regardless)
D-E2 EoP Future eval/new Function on retailer string → RCE SB-073 (lint ban; CF Workers runtime has no eval and no Node vm — defence-in-depth, not the primary control)
D-E3 EoP SSRF via M2 image fetcher — retailer imageUrl points at 169.254.169.254 (cloud metadata) or localhost:5433 SB-074 (undici custom Dispatcher connect hook: pre-resolve DNS, reject RFC1918 / RFC6598 / 127.0.0.0/8 / 169.254.0.0/16 / fe80::/10 / fc00::/7, then connect to validated IP literal — defeats DNS-rebinding)

Walk-through scenarios

Five named scenarios from the #44 brief. Each ends with the baseline-row IDs that close the chain and the residual gap.

Scenario 1 — Aggressive scraping of 961tech (competitive / DoS)

A competitor or AI-training crawler hits us at 100 req/sec across /products/*, /build, /api/* for hours. Worker invocation cost spikes; Neon connection pool stays bounded by Hyperdrive but still adds Postgres compute. Catalog (low secrecy) is exfilable but technically already public on retailer sites — the moat is the aggregation, not any single price.

  • Defends: SB-016 (Cloudflare Rate Limiting Rules — free tier 1 rule scoped to /api/* POST), SB-026 (Bot Fight Mode, free), SB-005 (Hyperdrive bounded fan-in), SB-067 (soft-stale catalog tolerates parser/scraper degradation).
  • Tension: legitimate AI / search indexing is desirable for Casual-track SEO (#47). Block aggressive scraping behaviour without blocking well-behaved bots — this means rate-shape on volume + cadence, not blanket UA bans.
  • Residual: a determined adversary will defeat Bot Fight Mode + Rate Limiting Rules. Super Bot Fight Mode is Pro-tier; Bot Management is Enterprise-only (CF Bots docs). Cost question for RFC-0006.

Scenario 2 — Compromised retailer site

A retailer is breached. Attacker injects a malicious product title (<script>alert(1)</script>), a tracking-pixel imageUrl, a Listing.url pointing at a credential-harvesting clone, and drops one flagship-GPU price to $0.01.

  • Defends: SB-053 (no dangerouslySetInnerHTML on retailer-derived strings; React auto-escapes), SB-054 + SB-055 (image-host allow-list + Content-Type filter), SB-056 (rolling-median anomaly hold for the $0.01 price), SB-023 + SB-024 (URL host allow-list at click-redirect — already partially in place via DB lookup, but admin-write tooling #16 re-opens this).
  • Residual: semantically wrong but structurally valid poisoning (price drops 40 %, not 60 %; spec value plausible-but-wrong). Mitigated only operationally — cross-retailer disagreement detection (#43).

Scenario 3 — Account takeover via OAuth (M2)

Attacker registers GitHub OAuth using victim@gmail.com (GitHub does not always require ownership proof for non-primary email). Victim later signs in via Google with the same email. If allowDangerousEmailAccountLinking: true on either provider, accounts auto-merge → attacker now signs into victim's data.

sequenceDiagram
    participant Attacker
    participant GitHub
    participant 961tech as 961tech (Auth.js v5)
    participant Victim
    participant Google
    Attacker->>GitHub: register, claim victim@gmail.com (unverified)
    Attacker->>961tech: sign in with GitHub
    961tech->>961tech: create User (email=victim@gmail.com)
    Note over Victim,Google: weeks later
    Victim->>Google: sign in
    Google->>961tech: callback (sub=victim, email_verified=true)
    961tech->>961tech: callbacks.signIn invoked
    alt allowDangerousEmailAccountLinking = true (DANGER)
        961tech->>961tech: merge into Attacker's User row
        961tech->>Victim: signed in, but as Attacker's account
    else default v5 behaviour
        961tech->>Victim: error / prompt to confirm explicit linking
    end
  • Defends: Auth.js v5 default allowDangerousEmailAccountLinking: false (authjs.dev/reference/core/providers). Reinforced by SB-040 (set explicit), SB-041 (callbacks.signIn rejects providers reporting email_verified === false).
  • Residual: a verified-email-on-attacker-provider attack still works in theory. Closing requires per-provider trust differentials, which is not the v5 default model. Acceptable for hobby scale; reconsider for higher-value features.

Scenario 4 — Affiliate-click spoofing

Bot farm hits /api/go/r/X/p/Y 10 k times across rotating IPs to inflate click counts on retailer X. If #17 lands a CPC-payout flow, this becomes direct revenue theft.

  • Defends: SB-016 (CF Rate Limiting — gross volume), SB-017 (per-(ipHash, listingId) token bucket — targeted-listing inflation), SB-021 (CF-Connecting-IP not XFF — denies trivial source spoofing), SB-022 (single-use opaque click token + redeemedAt column for atomic redemption + 24 h TTL), SB-027 (Turnstile interstitial as escalation lever; UX cost noted), SB-028 (two-phase initiatedlanded accounting).
  • Residual: sophisticated headless-bot click inflation that defeats Bot Fight Mode + Rate Limiting Rules. Closes only with Super Bot Fight Mode (Pro) or Turnstile-on-every-click (UX tax). Open question for RFC-0006.

Scenario 5 — Listing-data poisoning via SQL injection

Attacker tries '; DROP TABLE Listing; -- payloads through every input — source querystring, future admin form, future API.

  • Defends: Prisma uses parameterised queries by default — classical SQLi vector closed. Documented in SB-058 callout. Remaining injection vectors: (a) the Json blob (Product.specs) rendered as a template anywhere; (b) any future raw SQL identifier interpolation. Both must be explicitly guarded via SB-058 (Zod-per-category) and an explicit "never use string-interp on retailer-derived strings" rule.
  • Residual: a future contributor adds a raw $queryRawUnsafe and skips review. Mitigated only by code-review discipline; consider an ESLint rule banning $queryRawUnsafe outside of an audited migration path.

What this model does not cover

  • Live exploit walkthroughs or pentest output — this is theoretical, control-driven.
  • Dependency-CVE tracking beyond the freshness-band call-out in tech-stack.md § Cross-cutting risks. npm audit gating is one of the controls in the baseline (SB-080); the threat model itself doesn't enumerate CVEs.
  • Code-level audit of every route — only /api/go/* is read in detail. Surface A's mitigations apply to every route handler and server action; the baseline is the enforcement layer, not this page.
  • DR / RTO / RPO — pulled along by the RFC-0001 hosting decision.
  • Retailer self-onboarding (UC-K, M2) — deferred. When #36 revisits retailer self-onboarding, this page gets a new surface and re-walks the relevant scenarios.

Cross-references