Files
anwar-pi-website/SECURITY.md
AHMED 26d662ed7c fix: revert to next 15.4.11 (payload peer ceiling), record CVE risk acceptance
@payloadcms/next@3.90.2 peer-requires next <15.5.0; 15.5.27 was an
unsupported combo. Owner chose the pin + documented risk (SECURITY.md
§8). Transitive overrides (undici/sharp/postcss/sass) stay.
Gate skipped: parallel session WIP still in tree.
2026-10-13 06:02:15 +03:00

7.6 KiB

SECURITY — anwarpy.ai

Threat model: a solo-writer content site. The assets are the content, the member trust (ADR-0005), and the pipeline key. Not a fintech app — but every rule below is load-bearing.

1. Authentication

  • Payload auth (email + password). Signup writes verification_token + emailVerified flag at creation (ADR-0005 trust model); verification/resend routes: /api/auth/verify-email, /api/auth/resend-verification. Forgot/reset via Payload's built-in flow.
  • Session seam: src/lib/viewer.ts getViewer() is the only place components read identity. If auth provider changes (anticipated: Google OAuth or Clerk — owner decision 2026-10-11), it changes behind this seam; callers keep consuming Viewer.
  • No password rules theater: one live hint (strength guidance) at signup; no confirm-password field (owner decision, recorded).
  • Lockout: login attempts on /api/users are rate-limited by src/middleware.ts; the 429 message stays honest («too many attempts» fidelity — step 14 evidence). No CAPTCHA (owner decision, recorded 2026-10-10).
  • Verification emails go through Cloudflare Email REST (ADR-0003, never SMTP). Transport down → the UI/API says so in Arabic («خدمة البريد غير مربوطة بعد.»); it does not pretend success.

2. Environment variables & secrets

Var Scope Rule
DATABASE_URI / DB_PASSWORD server compose builds the URI; never commit, never echo
PAYLOAD_SECRET server signing/encryption; rotate = invalidate sessions
AGENT_API_KEY server pipeline auth; header x-agent-key
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, MAIL_FROM server mail transport
LAUNCH_MODE server launch policy (SECURITY-relevant: gates paid flows)
NEXT_PUBLIC_SITE_URL public canonical/og origin
STRIPE_SECRET_KEY, STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET server Phase 3
SEED_TEST_USERS dev only never set in prod

Rules:

  • .env* files are gitignored and excluded from rsync deploys; prod secrets live only in /root/anwarpi/.env.production.
  • Never print secret values into transcripts, docs, videos, or evidence. Known incident: 8 chars of prod DB_PASSWORD leaked in a session transcript on 2026-10-10 → owner to rotate; rotation is the standing remedy for any partial leak.
  • Read secrets at runtime via process.env inside server modules only; never in 'use client' code (only NEXT_PUBLIC_* there).

3. RBAC & content access

  • Two tiers: role: 'admin' (Ahmed, /admin + agent pipeline ownership) and everyone else (member/public). No roles table beyond that until a real need.
  • Content ACL is the crown jewel (ARCHITECTURE §3): Posts.content field-level read rule — public iff status=published && (access=free || freeEarlyAccess()). VIP previews (H2 list, dek) are the only VIP fields served to logged-out users when billing is on.
  • Files: GET /api/files/[id] enforces per-file access before streaming; the download gate and the article page must agree (one decision, ARCHITECTURE §3).
  • Admin UI access = Payload's built-in admin gate; never hand-roll admin pages on the frontend.

4. Input handling & pipeline hardening

  • pickFields on the agent API strips privileged fields (access, readingMinutes, status except explicit publish: true opt-in on POST; PUT cannot set status).
  • scanForSecrets / scanAgentPayload (src/lib/secret-scan.ts): POST and PUT scan the real text (Lexical → lexicalToText, or the raw string) for AWS AKIA keys, PEM blocks, sk- API keys, GitHub tokens (ghp_…), generic credential shapes → 422 with pattern NAMES only. Never echo the matched text, never log it.
  • String content → 400 (Lexical JSON required); no silent placeholder substitution.
  • Comment bodies: textarea, moderated (pending until approved), author forced server-side from the session — client-supplied author/authorName identity is ignored/overwritten (ADR-0002). Display names are a snapshot, not a live join, so renamed users can't retroactively forge old comments.
  • All user-facing error strings are fixed Arabic sentences from the route handlers — errors never reflect raw input back (no echo = reflected-input hygiene).

5. Transport & external calls

  • Everything behind Traefik with TLS on anwarpy.ai; www → apex 308.
  • Outbound: Cloudflare Email REST with scoped token; Stripe webhook verifies signature (STRIPE_WEBHOOK_SECRET) before trusting the body; agent API requires the key header (Authorization Bearer → 401).
  • The db container publishes no ports; prod DB access for maintenance = SSH tunnel (AGENTS.md rulebook recipe), never a public 5432.

6. Operational security

  • Backups: web/scripts/backup-db.sh → gzipped pg_dump; rehearsed restore 2026-10-10 (temp DB, count check, drop). Backup artifacts stay on the VPS backups/ dir, never in git, never in the deploy rsync.
  • Test rows (smoke signups, notify rows) use @example.com addresses and are deleted after verification — prod counts (users=6, launch_notifications=0) are the post-cleanup ground truth.
  • Media/files volumes are excluded from rsync (no --delete protects them).
  • Incident rule: any suspected leak (secret in transcript/log/URL) → rotate the credential first, then investigate. Recording the incident in docs is required; pretending it didn't happen is not.

7. What NOT to add (recorded decisions)

  • No CAPTCHA, no aggressive rate limits beyond login lockout (owner, 2026-10-10).
  • No WAF/Cloudflare proxy changes without owner approval — the domain's DNS/proxy is Ahmed's console.
  • No secrets in client bundles; no NEXT_PUBLIC_ for anything except the site URL.
  • No new auth provider without routing it through viewer.ts (see §1).

8. Accepted risks

next@15.4.11 — 28 known CVEs, max 9.5 (risk-accepted by owner, 2026-10-13)

  • Why pinned: @payloadcms/next@3.90.2 peer-supports next <15.5.0 || >=16.3.3 — no Payload 3.x release supports 15.5.x, so the safe ladder is 15.4.11 → (Payload 4 or a verified Next 16 migration). Forcing 15.5.x would run the CMS on an unsupported combo.
  • Exposure analysis: the 9.5-critical (Image Optimizer DoS, GHSA-9g9p-9gw9-jx7f) requires attacker-reachable remotePatterns — this app configures none (local covers only). The cache-poisoning advisory targets SSG/ISR caching; the site is predominantly dynamic SSR via the Payload Local API.
  • Revisit trigger: any of — Payload publishes 15.5/16 peer support (then upgrade next in one verified PR), Payload 4 adoption, or a new next advisory with remote-code-execution class impact.
  • Gate note: procoder check re-flags this on any package-lock.json change. If you must touch the lockfile before upgrading, either upgrade next in the same PR or skip the gate once with the reason in the commit message. Transitive CVE fixes already applied: undici 8.11.2, sharp 0.35.5, postcss 8.5.30, sass 1.105.1.