StatistaAmeri/SECURITY.md

15 KiB

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.