Resolves the findings from a full security & scalability audit. No user-facing behaviour changes for the normal signed-in flow.
Security
- SSRF protection (
lib/net-guard.ts, new). Every server-side fetch of a user-supplied URL now validates scheme (http/https only) and rejects hosts that resolve to private/loopback/link-local ranges (incl.169.254.169.254cloud metadata) — enforced both pre-flight and at connect time via an undici dispatcher (defeats redirect-to-internal + DNS rebinding). Applied to/api/unfurl(open-graph-scraper wired to the guarded dispatcher) and to the AI link/PDF enrichment fetches (lib/ai-enrich.ts). - Rate limiting (
lib/rate-limit.ts, new). Per-IP fixed-window limits on/api/unfurl,/api/github,/api/embed,/api/geocode(30/min) and/api/ai(20/min). - AI cost/abuse gate (
app/api/ai/route.ts). The AI route now requires a signed-in user before falling back to the host's API key; self-host single-user deploys opt back in withALLOW_ANON_AI=1. - Open redirect fixed (
app/auth/confirm/route.ts). Thenextparam now rejects protocol-relative (//host) and backslash forms. - Stored-XSS hardening (
lib/safe-url.ts, new). Card URL fields are scrubbed on ingestion from untrusted peers/imports, and anchorhref/ iframesrcvalues are guarded at render so ajavascript:URL can't execute. - Security headers (
next.config.mjs):X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy. - Config-file admin gate (
app/api/ai/config-file/route.ts): optionalAI_CONFIG_ADMIN_EMAILSallowlist for writing the shared host config. - Defense-in-depth: bounded the in-process caches (unfurl/geocode/ai-enrich),
capped AI enrichment fetch concurrency, added a
user_idfilter to Notion disconnect, and added anai_messagesretention trigger (keeps the latest 400 per space) —supabase/migrations/20260702000100_ai_messages_retention.sql.