291 lines
15 KiB
Markdown
291 lines
15 KiB
Markdown
# 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 `<script>`, inline event handlers, or URLs (`javascript:`).
|
|
|
|
**ALWAYS**
|
|
- Rely on framework auto-escaping (React/Vue/Svelte escape by default — keep it that way).
|
|
- Sanitize any HTML you *must* render with a maintained sanitizer (e.g. DOMPurify) and a strict allow-list.
|
|
- Set a strong **Content-Security-Policy** to limit script sources.
|
|
- Encode output for the correct context (HTML, attribute, JS, URL).
|
|
|
|
---
|
|
|
|
## 7. APIs, CORS & Rate Limiting
|
|
|
|
**NEVER**
|
|
- Set `Access-Control-Allow-Origin: *` together with credentials, or wildcard CORS on authenticated APIs.
|
|
- Ship public endpoints with no rate limiting (enables brute force, scraping, credential stuffing, cost-blowup, and DoS).
|
|
- Leave debug/admin/internal endpoints reachable in production.
|
|
- Return different responses that leak whether a record exists when it shouldn't.
|
|
|
|
**ALWAYS**
|
|
- Restrict CORS to an explicit allow-list of known origins.
|
|
- Rate-limit by IP and by user/account on auth, write, and expensive endpoints; add exponential backoff/lockout for auth.
|
|
- Require auth on every non-public endpoint and verify it server-side.
|
|
- Validate `Content-Type` and reject unexpected methods.
|
|
- Protect state-changing requests against **CSRF** (SameSite cookies + CSRF tokens, or token-based auth not stored in cookies).
|
|
- Treat webhooks as untrusted: verify signatures before acting.
|
|
|
|
---
|
|
|
|
## 8. Sessions, Tokens & JWTs
|
|
|
|
**NEVER**
|
|
- Accept JWTs with `alg: none` or let the client choose the algorithm.
|
|
- Sign JWTs with a weak/guessable/shared secret.
|
|
- Issue tokens with no expiry, or store sensitive long-lived tokens in `localStorage` (XSS-readable).
|
|
- Trust JWT claims without verifying the signature server-side.
|
|
|
|
**ALWAYS**
|
|
- Pin the expected algorithm and verify the signature on every request.
|
|
- Use short-lived access tokens + rotating refresh tokens.
|
|
- Prefer `httpOnly`, `Secure`, `SameSite` cookies for session tokens.
|
|
- Include and validate `exp`, `iss`, `aud`; revoke on logout/compromise.
|
|
|
|
---
|
|
|
|
## 9. File Uploads
|
|
|
|
**NEVER**
|
|
- Trust the client-provided filename, extension, or MIME type.
|
|
- Store uploads in a web-served directory where they can be executed.
|
|
- Allow arbitrary file types or unbounded file sizes.
|
|
|
|
**ALWAYS**
|
|
- Validate type by content (magic bytes), enforce a size limit, and allow-list extensions.
|
|
- Generate a new random server-side filename; never use the user's path.
|
|
- Store outside the web root or in object storage with no-execute and least-privilege access.
|
|
- Scan uploads where feasible and serve them from a separate domain/sandbox with `Content-Disposition`.
|
|
|
|
---
|
|
|
|
## 10. HTTP Security Headers & Transport
|
|
|
|
**ALWAYS** set (or have the framework/host set) at minimum:
|
|
- `Strict-Transport-Security` (HSTS) — and enforce HTTPS everywhere; redirect HTTP→HTTPS.
|
|
- `Content-Security-Policy` — restrict scripts/styles/connect sources.
|
|
- `X-Content-Type-Options: nosniff`
|
|
- `X-Frame-Options: DENY` (or CSP `frame-ancestors`) to prevent clickjacking.
|
|
- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
- Sensible `Permissions-Policy`.
|
|
|
|
**NEVER** serve auth or sensitive data over plain HTTP, or mix HTTP content into an HTTPS app.
|
|
|
|
---
|
|
|
|
## 11. Error Handling & Logging
|
|
|
|
**NEVER**
|
|
- Return stack traces, SQL errors, file paths, or internal details to the client.
|
|
- Log passwords, tokens, full card numbers, or other secrets/PII.
|
|
- Run production with debug mode on.
|
|
|
|
**ALWAYS**
|
|
- Return generic error messages to users; log detail server-side with correlation IDs.
|
|
- Log security-relevant events (auth failures, access denials, privilege changes) for audit.
|
|
- Scrub/redact sensitive fields before logging.
|
|
|
|
---
|
|
|
|
## 12. Dependencies & Supply Chain
|
|
|
|
**NEVER**
|
|
- Add a dependency you don't need, or pull random unvetted packages for a one-liner.
|
|
- Ignore known-vulnerable dependency warnings.
|
|
|
|
**ALWAYS**
|
|
- Run `npm audit` / `pip-audit` / `osv-scanner` (or equivalent) and fix high/critical issues.
|
|
- Pin versions and use a lockfile; keep dependencies updated.
|
|
- Verify package names carefully (watch for typosquats).
|
|
- Enable automated dependency security updates (e.g. Dependabot) where possible.
|
|
|
|
---
|
|
|
|
## 13. Sensitive Data & Privacy
|
|
|
|
**NEVER**
|
|
- Collect or store more personal data than the feature needs.
|
|
- Store sensitive data (PII, payment, health) unencrypted at rest.
|
|
- Send sensitive data to third-party services/analytics without need and consent.
|
|
|
|
**ALWAYS**
|
|
- Encrypt sensitive data at rest and in transit.
|
|
- Apply least-privilege access to PII; mask/redact in UIs and logs.
|
|
- Offboard payment handling to a PCI-compliant provider (e.g. Stripe) rather than touching card data yourself.
|
|
- Provide data deletion/export paths where regulations require.
|
|
|
|
---
|
|
|
|
## 14. AI / LLM-Specific (if the app calls an LLM)
|
|
|
|
**NEVER**
|
|
- Concatenate untrusted user input directly into a system prompt and treat the model output as trusted (**prompt injection**).
|
|
- Give the model unrestricted tools/DB/shell access driven by user-controlled text.
|
|
- Expose your LLM provider API key to the client.
|
|
|
|
**ALWAYS**
|
|
- Treat model output as untrusted input — validate and authorize any action it triggers.
|
|
- Keep the LLM key server-side and put your own rate limits + spend caps around it.
|
|
- Separate trusted instructions from untrusted user content; constrain tool use with allow-lists and per-action authorization.
|
|
- Filter/limit what data the model can retrieve based on the requesting user's permissions.
|
|
|
|
---
|
|
|
|
## 15. Server-Side Request & Redirect Safety
|
|
|
|
**NEVER**
|
|
- Fetch a user-supplied URL from the server without restriction (**SSRF** — can hit internal metadata endpoints / private network).
|
|
- Redirect to a user-supplied URL without validation (**open redirect** — aids phishing).
|
|
|
|
**ALWAYS**
|
|
- Allow-list domains/schemes for any server-side fetch; block private/internal IP ranges and metadata IPs.
|
|
- Validate redirect targets against an allow-list or use relative paths only.
|
|
|
|
---
|
|
|
|
## Pre-Ship Security Checklist
|
|
|
|
Run through this before any deploy. Treat unchecked items as blockers.
|
|
|
|
- [ ] No secrets in code, repo history, logs, or client bundles; `.env*` gitignored; keys rotated if ever exposed.
|
|
- [ ] Auth verified server-side on every protected route; passwords hashed with bcrypt/argon2; no default creds.
|
|
- [ ] Every data read/write is authorized by ownership/role server-side (no IDOR); privileged actions re-checked.
|
|
- [ ] RLS / DB rules enabled and scoped per-user; service-role key server-side only; no `if true` rules.
|
|
- [ ] All queries parameterized; all input validated server-side against a schema; mass assignment prevented.
|
|
- [ ] Output escaped/sanitized; CSP set; no unsanitized raw-HTML rendering.
|
|
- [ ] CORS locked to known origins; rate limiting on auth/write/expensive endpoints; CSRF handled; webhooks signature-verified.
|
|
- [ ] JWTs verify signature + pinned alg + expiry; session cookies `httpOnly`/`Secure`/`SameSite`.
|
|
- [ ] File uploads validated by content, size-limited, randomly named, stored no-execute.
|
|
- [ ] Security headers + HTTPS enforced (HSTS, nosniff, frame-ancestors, referrer-policy).
|
|
- [ ] Generic error messages to users; debug off in prod; no secrets/PII in logs.
|
|
- [ ] Dependency audit clean (no high/critical); lockfile committed.
|
|
- [ ] Sensitive data minimized + encrypted; payments via compliant provider.
|
|
- [ ] LLM key server-side; model output treated as untrusted; SSRF/open-redirect protections in place.
|
|
|
|
---
|
|
|
|
## How to use this file with the AI agent
|
|
|
|
- Keep this file at the project root as `SECURITY.md` and reference it in your `CLAUDE.md` / agent instructions: *"Follow all rules in SECURITY.md on every change."*
|
|
- When asking for a feature, you can add: *"Implement this per SECURITY.md and flag any rule it touches."*
|
|
- Periodically ask: *"Audit the current codebase against SECURITY.md and list violations by severity."*
|
|
- If the agent ever proposes weakening a control for convenience, that's a red flag — ask for a secure alternative instead.
|