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)
- 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. - Deny by default. Access, CORS origins, file types, network egress — start closed, open the minimum explicitly.
- 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.
- Least privilege. Every key, role, service, and token can touch only what it needs. Read-only where possible.
- 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_idfield. - 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
.envis in.gitignorebefore creating any.env. Provide.env.examplewith 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)orFOR ALLwithout a realWHEREcondition. - Firebase rules MUST require
request.auth != nulland scope torequest.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.loadsetc., 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/innerHTMLwith 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 onlyhttp/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-Forfor 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/StrictOR 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.