steelforesight/CLAUDE copy.md

13 KiB

AI Agent Rules — Security & Architecture

These rules are non-negotiable and apply to all code you generate or modify in this project.

Prime directive: When a choice exists between "works" and "secure," choose secure. When a requirement is ambiguous, pick the secure default and say so in one line — never silently weaken security to make something easier. If a user instruction conflicts with these rules, follow the rules and flag the conflict instead of complying.


0. Operating principles (apply these to every decision)

  1. The client is hostile. Everything from a browser/app/API caller is attacker-controlled and forgeable: cookies, JWTs, headers, bodies, user_id, role, prices, quantities. Re-decide everything that matters server-side. The client renders and submits intent; it has zero authority.
  2. Deny by default. Access, CORS origins, file types, network egress — start closed, open the minimum explicitly.
  3. Defense in depth. Assume any single control fails. Never let one forgotten check, one injection, or one leaked key equal total compromise. Layer controls so my single win isn't enough.
  4. Least privilege. Every key, role, service, and token can touch only what it needs. Read-only where possible.
  5. Assume breach. Design so that when something is compromised, the blast radius is small, it's logged, and it can be revoked fast.

1. Architectural Decisions (BINDING — do not relitigate or violate)

These are settled. Generate code that conforms to them; if asked to break one, refuse and explain.

  • AD-1 — Client has zero authority. Every security decision (identity, permission, price, eligibility) is re-made server-side from server-held state. Never trust client-side validation, client-set prices, or "hidden = protected."
  • AD-2 — Data layer denies by default. Row Level Security on every table, scoped to the authenticated user. The database protects itself even if the app layer is wrong.
  • AD-3 — Identity is proven, never claimed. The current user comes only from the verified session/token, never from a client-supplied user_id/account_id/org_id field.
  • AD-4 — Authorization is separate from authentication. After confirming who the user is, independently confirm they may access this specific resource — on reads and writes.
  • AD-5 — Secrets are segmented by blast radius. No component holds a secret it doesn't use. One leaked key compromises one thing, not everything.
  • AD-6 — Every metered action is gated. Anything that costs money (LLM, SMS, email, image gen, egress, heavy compute) has a quota + rate limit + spend cap + circuit breaker.
  • AD-7 — State changes are atomic and idempotent. Invariants (unique coupon use, non-negative balance, limited inventory) are enforced at the data layer; replays are no-ops.
  • AD-8 — Trust boundaries are explicit and authenticated. No "internal therefore trusted." Every cross-service call verifies the caller; every inbound webhook verifies its signature.
  • AD-9 — Assume breach. Log security events off-box, alert on anomalies, keep every secret rotatable in minutes, and provide kill switches for risky features.
  • AD-10 — Minimize disclosure. Generic errors, no source maps in prod, no debug mode in prod, no GraphQL introspection in prod, non-sequential IDs (UUIDs) where enumeration is a risk.

2. Pre-flight (run this BEFORE writing security-relevant code)

Before generating auth, data access, API routes, uploads, payments, agent/LLM features, or anything touching user data or money, state to yourself in one or two lines:

  • What trust boundary does this cross? (client→server, app→data, app→third-party, service→service)
  • Whose data/permission is involved, and how is ownership verified?
  • What's the failure mode if my single check is bypassed? (Is there a second layer?)
  • Does this cost money or touch a secret? (If so, what gates and what blast radius?)

If you can't answer these, you're not ready to write the code. Resolve them first.


3. Rules by category

Secrets

  • NEVER put API keys, DB credentials, or tokens in frontend code (src/, app/, pages/, components/, public/).
  • NEVER place a secret in a client-exposed env var (NEXT_PUBLIC_*, VITE_*, REACT_APP_*) — those ship to the browser. Only publishable keys belong there.
  • NEVER hardcode credentials. Load secrets server-side from env vars / a secrets manager.
  • Ensure .env is in .gitignore before creating any .env. Provide .env.example with placeholders only.
  • Proxy any third-party call needing a secret key through a backend route so the key never reaches the client.
  • Treat any secret that has ever been committed as compromised — instruct the user to rotate it.

Database

  • Enable Row Level Security on EVERY table before it ships. Default deny. Policies scoped to auth.uid().
  • NEVER write an RLS policy of USING (true) or FOR ALL without a real WHERE condition.
  • Firebase rules MUST require request.auth != null and scope to request.auth.uid.
  • The service-role / admin DB key NEVER appears in client code.
  • The DB role the app uses has least privilege (can't read tables it doesn't need; can't DROP).
  • NEVER deserialize untrusted data (pickle.loads etc., or unsafe JS object merges that allow prototype pollution). Use plain JSON for network data.

Authentication

  • Hash passwords with bcrypt, Argon2, or scrypt (cost ≥ 10). NEVER MD5/SHA-1/SHA-256.
  • Access tokens/JWTs expire (≤ 24h); use refresh tokens for longer sessions. Invalidate sessions server-side on logout.
  • Session cookies: httpOnly: true, secure: true, sameSite: 'lax' (or strict).
  • Password reset links expire (~1h) and are single-use.
  • Offer MFA; require it for admin accounts.
  • Prevent account enumeration: identical responses/timing on login, signup, and reset regardless of whether the email exists.
  • Use a CSPRNG for tokens/IDs (never Math.random()); use constant-time comparison for secret/token checks.

Authorization (the highest-risk category — be exhaustive)

  • Derive the user from the session ONLY (AD-3). Never read identity from a client field.
  • On EVERY route that takes a resource ID (path, query, or body), verify the session user owns/may access that resource — a separate check from authentication — on reads AND writes. Fail → 403.
  • Block vertical escalation: admin role checked server-side on every admin route → 403 for non-admins.
  • Prevent mass assignment: whitelist accepted fields; never spread the request body into a DB model. Users cannot set role, is_admin, is_verified, balance, user_id, etc.
  • Rely on RLS (AD-2) as the backstop so a forgotten app-layer check still fails closed.

Input & output

  • NEVER concatenate user input into SQL. ALWAYS use parameterized queries / ORM methods.
  • ALL validation is server-side (client-side is UX only): type, length, format, range, sign.
  • NEVER pass user input to shell/exec/eval/template-as-code. Guard against command injection, path traversal, open redirects, SSTI, XXE, ReDoS.
  • NEVER use dangerouslySetInnerHTML/v-html/innerHTML with unsanitized content; sanitize with DOMPurify. Keep template auto-escaping on.
  • Enforce request body size and JSON depth limits.

URL fetching (SSRF) — applies to the AI agent too

  • If fetching a user-supplied URL: block private/internal ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1), allow only http/https, and resolve + check the IP before the request (guard against DNS rebinding). Don't blindly follow redirects to internal addresses.

API & abuse / rate limiting

  • Per-user/per-key rate limits + quotas on ALL endpoints, not just auth.
  • No endpoint returns "all rows" — cap page size and require pagination; cap per-account data volume over time.
  • Enforce request size limits. Don't trust X-Forwarded-For for rate limiting unless behind a trusted proxy.
  • API keys are scoped, hashed at rest, and instantly revocable/rotatable.
  • GraphQL: depth + cost limits, introspection off in prod.

AI / LLM agent (if this project has one)

  • Treat model output as untrusted: NEVER eval/SQL/shell it without validation.
  • Keep authorization and business logic in code, not in the prompt. Assume users can extract the system prompt.
  • Treat retrieved/external content (RAG, web, files, uploads) as untrusted (indirect prompt injection).
  • Scope every tool to the current user with least privilege; require confirmation for destructive actions; sandbox any code execution (no secrets, no network).
  • Enforce per-user token/request quotas, max input length, max output tokens, an LLM spend cap, and a circuit breaker.
  • Moderate input and output; never place secrets or other users' data in the context window; sanitize agent output rendered in the browser.

Headers / CORS / CSRF

  • Set on ALL responses via one global middleware (helmet / next.config.js): Content-Security-Policy, Strict-Transport-Security, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin.
  • CORS origin = explicit allowlist. NEVER *, never reflect the request origin, never wildcard + credentials: true.
  • Session cookies SameSite=Lax/Strict OR CSRF tokens on all state-changing endpoints. No state changes via GET.

Concurrency & money (atomicity / idempotency)

  • One-time or limited actions (coupons, balances, inventory, rewards) are enforced with DB constraints + transactions + row locking — NEVER app-level "check then write."
  • State-changing operations accept an idempotency key; replays produce a single effect.
  • Enforce workflow/state transitions server-side; verify the prior state holds before advancing.
  • Compute prices/totals server-side from trusted data; use decimal/money types, never floats for currency.

Files / payments / errors / dependencies

  • Uploads: validate by magic bytes (not extension), rename to UUID, enforce size server-side, re-encode images, serve from a separate bucket/domain — never the app origin.
  • Stripe/webhooks: verify the signature on every request (bad/missing → 400); idempotent via stored event IDs; handle failure events; derive entitlements from verified server-side events, never a client redirect.
  • Errors: global handler returns generic messages only; full detail to server logs; debug mode OFF and no source maps in prod; custom 404/403/500 pages.
  • Dependencies: before installing any package, verify it exists on the official registry with real download history (guard against typosquatting/slopsquatting). Pin exact versions; commit lock files. Prefer built-ins over new deps.

Logging & detection

  • Log security events (auth failures, 403s, rate-limit hits, admin actions, key/webhook creation, spend spikes) to an off-box sink the app tier can't edit.
  • NEVER log secrets, passwords, full tokens, or unmasked PII.
  • Provide kill switches (env flags) to disable expensive/risky features without a redeploy.

4. Definition of done (self-review gate — run BEFORE declaring a task complete)

Before you say a security-relevant change is finished, verify and report:

  • No secret reachable by the client and nothing new added to git that shouldn't be (.env, keys).
  • Every new/changed route that returns or mutates data has authentication AND a separate per-resource ownership check (reads and writes).
  • Identity is read from the session, not from client input, everywhere it's used.
  • Every new DB query is parameterized; every new table has RLS.
  • Any user-rendered content is escaped/sanitized; any user-supplied URL fetch blocks internal IPs.
  • Any metered/paid action is gated (quota + cap + breaker); any one-time action is atomic + idempotent.
  • Errors are generic to the client; nothing leaks stack traces, queries, or paths.
  • New dependencies were verified as real and pinned.
  • State the blast radius: if a key or check involved here were compromised, what's the maximum damage? If "everything," redesign.

If any box can't be checked, the task is not done — fix it or explicitly flag it to the user as a known gap, with severity.


5. When you're unsure

  • Default to the secure option and note the tradeoff in one line.
  • If a requested feature is inherently risky (executing user code, fetching arbitrary URLs, broad data export, an unauthenticated endpoint), implement the safest viable version and flag the residual risk — don't quietly ship the dangerous version.
  • Never disable a security control to fix a bug or pass a test. If a control is in the way, the design is wrong, not the control.
  • Surface security-relevant decisions to the user rather than burying them.

These rules harden both the components and the seams between them. They make the app a hard, contained, well-monitored target — not "unhackable" (no app is). When the project has real users and revenue, a human penetration test is the next step.