169 lines
13 KiB
Markdown
169 lines
13 KiB
Markdown
<!--
|
|
SAVE THIS FILE AS:
|
|
• CLAUDE.md → Claude Code
|
|
• AGENTS.md → Cursor, GitHub Copilot, Codex, Windsurf, Gemini CLI
|
|
• both → if unsure, create both (identical contents)
|
|
Put it in your PROJECT ROOT and commit it. Your AI coding tool reads it
|
|
automatically on every task from then on.
|
|
-->
|
|
|
|
# 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.*
|