# SECURITY.md — Security Rules for This Project > **To the AI agent reading this:** These are **mandatory, non-negotiable rules**. Follow them on every change you make, even when the user doesn't mention security. When a requested feature conflicts with a rule here, implement the secure version and tell the user what you did and why. If you are unsure whether something is safe, **stop and ask** rather than guessing. Never disable a security control to "make it work" — fix the underlying cause instead. This file exists because apps built fast (including AI-assisted ones) repeatedly ship the same handful of vulnerabilities. The goal is an app that is "immune" to the common classes of attack by **default**, not as an afterthought. --- ## 0. The Golden Rules (read these first) 1. **The frontend is public.** Anything in client-side code, network responses, or the browser is visible to and editable by an attacker. Never trust it. All real security must live on the server / database. 2. **Never trust input.** Validate, sanitize, and authorize *every* input on the server — query params, headers, body, file uploads, cookies, and webhook payloads. 3. **Authenticate, then authorize, on every request.** Logging in is not enough. Check *this specific user is allowed to do this specific action on this specific resource* — server-side, every time. 4. **Secrets stay on the server.** No keys, tokens, passwords, or connection strings in frontend code, repos, or logs. Ever. 5. **Fail closed.** When something is ambiguous or errors out, deny access. Default to the most restrictive option. 6. **Least privilege everywhere.** Every key, role, token, and DB account gets the minimum permissions it needs and nothing more. --- ## 1. Secrets & Credentials **NEVER** - Hardcode API keys, passwords, JWT secrets, DB URLs, or private tokens anywhere in source. - Put secrets in frontend/client code or in any `NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, or otherwise client-exposed env var. - Commit `.env`, `.env.local`, credential files, `*.pem`, or service-account JSON to git. - Log secrets, tokens, or full request bodies that may contain them. - Reuse the same secret across dev/staging/prod. **ALWAYS** - Load secrets from server-side environment variables only. - Add `.env*`, `*.pem`, `*.key`, `secrets/`, and credential files to `.gitignore` *before* the first commit. - Provide a `.env.example` with key **names only** and dummy values. - Use a distinct, high-entropy (32+ random bytes) value for each signing/encryption secret. - Assume any secret that ever touched the frontend or a public repo is **compromised** — rotate it. - Use a secrets manager (or the platform's encrypted env store) in production, not files on disk. --- ## 2. Authentication **NEVER** - Roll your own crypto, password hashing, or session logic when a vetted library/provider exists. - Store passwords in plaintext or with fast/weak hashes (MD5, SHA1, unsalted SHA256). - Trust a `userId`, `role`, `isAdmin`, or `email` value that came from the client to decide who someone is. - Put auth state only in `localStorage` and treat its presence as proof of identity. - Leave default/seed credentials (`admin/admin`) in any environment. - Return "user not found" vs "wrong password" differently (enables user enumeration). **ALWAYS** - Use a maintained auth library/provider (e.g. the platform's auth, Auth.js, Lucia, Clerk, Supabase Auth) and follow its server-side session verification. - Hash passwords with bcrypt, scrypt, or Argon2 with a proper cost factor. - Verify the session/token **on the server** for every protected request and derive identity from it — never from request body params. - Enforce a password policy + check against known-breached passwords where possible. - Implement account lockout / throttling on repeated failed logins. - Offer/encourage MFA for sensitive accounts. - Expire sessions, rotate on privilege change, and invalidate on logout server-side. --- ## 3. Authorization (the #1 vibe-coded flaw) > Most rapidly-built apps authenticate but barely authorize. This is where IDOR, broken access control, and data leaks come from. **NEVER** - Hide a button/route on the frontend and call it "protected." UI hiding is not access control. - Use sequential/guessable IDs as the *only* thing protecting a resource (`/api/orders/1043` → an attacker just tries `1044`). This is **IDOR**. - Assume that because the UI only sends valid requests, the server only receives valid requests. - Let one user's token read/modify another user's data because the query didn't filter by owner. **ALWAYS** - On every data access, check ownership/permission server-side: `WHERE owner_id = :current_user` (or equivalent), not just `WHERE id = :id`. - Re-check role/permission for every privileged action on the server, on every call. - Enforce authorization at the **lowest layer possible** (DB row-level security + API layer), so a missed check in one place doesn't expose everything. - Use unguessable IDs (UUIDs/ULIDs) for resources, *and still* authorize them. - Centralize authorization logic so it's consistent and auditable, not copy-pasted per route. --- ## 4. Database Security (Supabase / Firebase / direct DB) > Vibe-coded apps frequently expose the DB straight to the browser with permissive rules. This is catastrophic. **NEVER** - Ship Supabase/Postgres tables with **Row Level Security (RLS) disabled** on anything holding user data. - Use Firebase/Firestore rules like `allow read, write: if true;` or test-mode rules in production. - Expose a service-role / admin DB key to the client (the anon/public key is the only client-side key, and even that needs RLS behind it). - Build SQL by string concatenation or template interpolation with user input. - Run app queries as a superuser/admin DB account. **ALWAYS** - Enable RLS on every table and write explicit policies scoped to the authenticated user. - Write Firestore/RTDB rules that check `request.auth` and validate ownership and shape. - Use parameterized queries / prepared statements / a query builder / ORM bindings — **always** (prevents SQL injection). - Give the app a least-privilege DB role (no DROP/ALTER, only needed tables). - Validate data shape and types at the DB boundary, not just the UI. - Keep the service-role key strictly server-side for trusted admin operations. --- ## 5. Input Validation & Injection **NEVER** - Pass user input directly into SQL, shell commands, `eval`, template engines, `dangerouslySetInnerHTML`, file paths, or `Function()`. - Rely on frontend validation as a security boundary (it's UX only). - Build file paths from user input without normalizing (enables **path traversal**, `../../etc/passwd`). **ALWAYS** - Validate every input server-side against a strict schema (e.g. Zod, Pydantic, Joi) — type, length, format, allowed values. - Allow-list rather than block-list whenever possible. - Use parameterized queries for SQL and safe APIs for OS/command operations (avoid shelling out with user input at all). - Reject unexpected fields to prevent **mass assignment** (don't blindly spread `req.body` into a DB record — pick allowed fields explicitly). - Canonicalize and confine file paths to an intended directory. --- ## 6. Output / XSS / Content **NEVER** - Render untrusted content as raw HTML (`dangerouslySetInnerHTML`, `innerHTML`, `v-html`, `{{{ }}}`) without sanitizing. - Reflect user input into responses unescaped. - Inject user-controlled values into `