StatistaAmeri/SECURITY-CHECKLIST.md

55 KiB
Raw Blame History

🔒 The Comprehensive Security Checklist for Vibe-Coded Apps & AI Agents

A merged, expanded checklist built from the vibe-check ruleset (17 categories) and Sherlock Forensics' 7 prompts, plus the things both of them miss: anti-scraping, API-abuse prevention, AI/LLM-agent security, infrastructure hardening, and monitoring.

Goal: make it genuinely hard for a random person — or a bored kid with a script — to break in, scrape your data, or run up your bill.


⚠️ Read this first (honest reality check)

There is no such thing as "unhackable." Anyone who tells you otherwise is selling something. What you can do is raise the cost of attacking you so high that casual attackers move on and serious ones are slowed down and detected. That's the realistic goal: defense in depth, not invincibility.

This checklist gets you past the mistakes that have actually taken down real vibe-coded apps in production — none of which required a sophisticated attack. When you have real users and real data, hire a human penetration tester. No checklist replaces someone actively trying to break your stuff.

Two stats worth remembering:

  • Carnegie Mellon found AI-generated code is ~61% functionally correct but only ~10.5% secure. The gap between "it works" and "it's safe" is where you get breached.
  • Broken access control (someone reading another user's data by changing an ID) has been the #1 web vulnerability for years. It's also the easiest to introduce and the easiest to exploit.

How to use this checklist (3 ways)

This document does three jobs at once. Use all three.

1. As rules your AI coding agent reads while it writes (prevention). Copy the Agent Rules section at the bottom into your project root as CLAUDE.md (Claude Code) or AGENTS.md (Cursor, Copilot, Codex, Windsurf, Gemini CLI). Commit it. Your AI reads it automatically from then on and stops creating these vulnerabilities in the first place.

2. As an audit your AI agent runs against existing code (detection). Paste the prompts in Copy-Paste AI Audit Prompts into your AI tool, one at a time. Tell it to investigate, report what it found, fix it, and verify. Don't batch them — one category at a time, fully, or it skims.

3. As manual tests you run yourself (verification). Work through Manual Penetration Tests. These catch the things AI can't verify from inside the code — like whether your live database is actually queryable from the open internet right now.

If your product itself is an AI agent (a chatbot, an LLM-powered assistant, an autonomous tool-using agent), pay special attention to Section 7: AI / LLM Agent Security. That's the attack surface that doesn't exist in normal apps and that neither source covers.


🚨 TIER 0 — The five that actually take companies down

If you do nothing else today, do these five. Every documented vibe-coded breach traces back to one of them. None required a clever attack.

  • Database is not publicly queryable. Row Level Security (RLS) enabled on every table, default-deny, policies scoped to the logged-in user. Test it with a raw curl using only your public/anon key (see Manual Test #1).
  • Every protected API route rejects unauthenticated requests with a 401, before the handler runs — not checked inside the function where it's easy to forget.
  • No secrets in your git repo. .env is in .gitignore before the first commit. No API keys, DB passwords, or tokens anywhere in source. Scan git history, not just current files.
  • Users can't access each other's data by changing an ID. Every route that takes a resource ID verifies current_user owns that resource — a separate check from "are you logged in."
  • No secret keys in the browser. Open DevTools → Sources → search for sk_, AKIA, secret, private_key. Secret keys live server-side only; the frontend gets publishable keys at most.

Everything below makes you more secure. These five keep you out of the headlines.


1. 🔑 Secrets & Credentials

The fastest way to get owned: ship a secret key to the browser or commit it to GitHub. Bots scan public repos for leaked keys within minutes of a push.

  • .env is listed in .gitignore before the first commit is ever made.
  • git ls-files .env returns nothing (the file is not tracked).
  • No API keys, DB credentials, or tokens are hardcoded in any source file.
  • No secret lives in a client-exposed env var: anything prefixed NEXT_PUBLIC_, VITE_, or REACT_APP_ is bundled into the browser — only publishable keys belong there.
  • .env.example exists with placeholder values only, never real credentials.
  • Git history has been scanned for previously-committed secrets (use gitleaks detect --source . --verbose). A deleted secret still lives in history.
  • Any key that was ever committed has been rotated. Once it touches a public repo, treat it as compromised — even if you deleted it seconds later.
  • Secrets are loaded server-side only and injected via your host's secret manager in production (Vercel/Netlify/Railway env vars, AWS Secrets Manager, Doppler, etc.), not shipped in a file.

Fix pattern: Move every secret to a server-only environment variable. Proxy any third-party call that needs a secret key through your own backend route so the key never reaches the client.


2. 🗄️ Database Security

Your database is the prize. If the anon/public key can read tables directly, you have no security at all — the rest of the app is decoration.

  • Row Level Security (RLS) is enabled on EVERY table before deployment (Supabase). No exceptions, including "internal" tables.
  • Every table has explicit policies scoped to the user (auth.uid()), with a default of deny all.
  • No policy uses USING (true) or FOR ALL without a real WHERE condition. That's an "allow everyone everything" policy in disguise.
  • Firebase rules require request.auth != null and scope access to request.auth.uid. The default open ruleset is wide open to the planet.
  • A curl to your REST endpoint with only the public/anon key returns empty or 403 for tables that hold user data (see Manual Test #1).
  • Service-role / admin database keys are never exposed to the client and never used in frontend code.
  • Never deserialize untrusted data. No pickle.loads/pickle.load (Python) or equivalent on anything a user can influence. Use JSON for all network data.
  • Database backups are enabled and the restore process has actually been tested (a backup you've never restored is a hope, not a backup).
  • The DB user your app connects with has least privilege — it can't DROP TABLE or read tables it doesn't need.

3. 👤 Authentication

Authentication is the front door. AI builds login flows that look complete but skip the security requirements.

  • Passwords are hashed with bcrypt, Argon2, or scrypt (cost factor ≥ 10). Never MD5, SHA-1, or plain SHA-256 — those are crackable in bulk if your DB leaks.
  • Login, registration, and password-reset endpoints are rate limited (see Section 14).
  • JWTs / session tokens expire (≤ 24 hours for access tokens; use short-lived access + refresh tokens for longer sessions).
  • Session tokens are actually invalidated on logout — server-side, not just deleted from the browser.
  • Password reset links expire within ~1 hour and are single-use.
  • Session cookies are set with httpOnly: true, secure: true, and sameSite: 'lax' (or strict).
  • Multi-factor authentication (MFA/2FA) is available, and required for admin accounts.
  • Account enumeration is prevented: login, signup, and password-reset return the same message and timing whether or not the email exists ("If that account exists, we sent a link"). Don't tell attackers which emails are registered.
  • New passwords are checked against breach lists (e.g. Have I Been Pwned's k-anonymity API) and a minimum length is enforced (length beats complexity rules).
  • Email/identity is verified before granting full access where it matters.
  • After N failed logins, the account or IP is throttled or temporarily locked (with care not to enable a lockout-as-DoS).

Using a managed auth provider (Supabase Auth, Auth0, Clerk, Firebase Auth)? Most of the hashing/session mechanics are handled for you — but you still own MFA config, enumeration behavior, session expiry settings, and authorization (Section 4).


4. 🛂 Authorization & Access Control (IDOR)

This is the big one. Authentication asks "who are you?" Authorization asks "are you allowed to touch this specific thing?" AI almost always builds the first and forgets the second. Changing /api/orders/123 to /api/orders/124 and getting someone else's order is the single most common — and most damaging — vibe-coded vulnerability.

  • Every route that takes a resource ID verifies ownership: current_user.id == resource.owner_id. This is a check you write, separate from authentication.
  • The ownership check exists on reads (GET) and writes (PUT/PATCH/DELETE) — not just writes. Reading someone else's data is still a breach.
  • Failing the ownership check returns 403 (not 404, not the data).
  • Vertical privilege escalation is blocked: a regular user hitting an admin endpoint gets 403. Admin status is checked server-side on every admin route.
  • Mass assignment / over-posting is prevented: a user can't set fields like role: "admin", is_verified: true, account_balance: 9999, or user_id: <someone_else> just by adding them to the request body. Whitelist the fields you accept; never blindly spread the request body into your DB model.
  • IDs that don't need to be guessable use UUIDs, not sequential integers (defense in depth — UUIDs don't replace ownership checks, they just make enumeration harder).
  • Authorization is enforced at the API/server layer, never relying on the frontend hiding a button. Hidden ≠ protected.

Fix pattern: In every handler, after you know who the user is, fetch the resource and explicitly compare ownership before returning or modifying it. Centralize this so it's hard to forget.


5. 🔌 API Security & Abuse Prevention

You specifically asked about people misusing your API. Neither source covers this in depth — here's the dedicated section.

An exposed API is an open invitation to automate abuse: scraping every record, hammering expensive endpoints, brute-forcing, or running up your metered bills (OpenAI, Twilio, SMS, etc.). Auth alone doesn't stop a legitimate user from abusing the API.

Access & authentication

  • Every non-public endpoint requires authentication (API key, OAuth token, or session) — verified server-side before the handler.
  • API keys are scoped (least privilege — read-only keys exist, keys are tied to specific resources/permissions) and revocable instantly.
  • Keys can be rotated without downtime, and you have a tested procedure to kill a leaked key.
  • Keys are hashed at rest in your DB (store a hash, show the raw key only once at creation).

Quotas, throttling & cost control

  • Per-key / per-user rate limits exist on all endpoints, not just login. Tier them (e.g. free vs paid).
  • Hard usage quotas per key per day/month, so one key can't drain your whole budget.
  • Spend caps / budget alerts on every metered downstream service (OpenAI, Anthropic, Twilio, SendGrid, cloud egress). Set provider-side billing limits and your own circuit breaker that stops calling out when a threshold is hit.
  • Request size limits (max body size, max upload size) enforced server-side to prevent memory-exhaustion DoS.
  • Result-set caps: list/search endpoints have a maximum page size and require pagination. No endpoint returns "all rows" — that's a scraping endpoint waiting to happen.

Anti-enumeration & data-volume protection

  • Endpoints that return lists cap how much can be pulled over a time window (a user pulling 50,000 records in an hour is exfiltration, not usage).
  • Sequential/guessable IDs in API responses are avoided where they enable bulk enumeration.
  • GraphQL specifically: query depth limiting, query cost/complexity analysis, disable introspection in production, and cap aliases/batching (otherwise one request can ask for everything).

Monitoring

  • Per-key usage is logged and anomalies trigger alerts (sudden 100× spike, access from new geography, hitting endpoints in an automated pattern).
  • Abusive keys can be auto-throttled or suspended when they cross thresholds.

Fix pattern: Put a rate-limiter + quota layer in front of the API (e.g. Upstash/Redis token bucket, or your gateway's built-in limits). Add a kill-switch env flag that disables expensive downstream calls if spend spikes.


6. 🕷️ Anti-Scraping & Bot Protection

You specifically asked about someone scraping your entire website. Neither source covers this — here's the dedicated section.

A motivated kid with a Python script and requests can copy your whole site in an afternoon if nothing stops them. You can't make scraping impossible (anything a browser can render, a determined bot can read), but you can make it slow, expensive, and detectable so casual scrapers give up.

Front-line bot management

  • Put the site behind a provider with bot management / WAF (Cloudflare, AWS WAF, Vercel's protections, Fastly). This is the single highest-leverage move — turn on bot-fight mode and managed challenge rules.
  • Rate limit by IP and by user, site-wide, not just on auth. Burst limits + sustained limits.
  • Challenge suspicious traffic with CAPTCHA / Turnstile / hCaptcha on high-value or high-volume flows (signup, search, bulk-data pages, password reset).
  • Block or challenge known datacenter / cloud IP ranges and bad ASNs for sensitive endpoints — most scraping comes from AWS/GCP/Azure/OVH IPs, not residential ones.

Make bulk extraction expensive

  • No endpoint dumps everything. Paginate, cap page size, and rate-limit pagination so walking all pages takes a long, throttled time.
  • Sensitive data (full contact lists, pricing, proprietary content) requires auth and is metered per account.
  • Consider honeypot links/fields invisible to humans but followed by naive crawlers — hitting them flags the client as a bot.
  • Optionally watermark or canary your data (unique seeded records) so you can identify who leaked/scraped it later.

Hygiene & detection

  • robots.txt is configured (won't stop bad actors, but stops well-behaved crawlers and is good hygiene). Don't rely on it for security.
  • Detect automation signals: missing/!suspicious User-Agent, no JS execution, headless-browser fingerprints, impossible click speeds, identical timing patterns. Feed these into your bot rules.
  • Monitor for scraping patterns: one client touching thousands of distinct resource pages, sequential ID walking, or high request volume with no normal browsing behavior → alert and throttle.
  • DDoS protection is enabled at the edge (your CDN/WAF), so a flood can't take you offline or rack up egress costs.

Reality check: Public content that's readable in a browser is ultimately scrapable. The goal is to stop the casual "scrape the whole site in one script" attempt and to gate/meter the data that actually matters behind auth + per-account limits.


7. 🤖 AI / LLM Agent Security (the part everyone forgets)

If your product is itself an AI agent — a chatbot, an LLM assistant, a tool-using autonomous agent — this is your unique attack surface. Neither source touches it. This is where "my AI agent gets abused" actually happens.

An LLM-powered feature introduces risks that don't exist in normal apps: users can talk the model into ignoring your rules, leaking data, calling tools it shouldn't, or burning your token budget.

Prompt injection (the #1 LLM risk)

  • Treat all model output as untrusted. Never eval() it, never run it as SQL/shell, never auto-execute it without validation. The model can be tricked into emitting malicious instructions.
  • Direct prompt injection: assume users will try "ignore previous instructions." Keep the real authorization/business logic in your code, not in the prompt. The system prompt is not a security boundary — server-side checks are.
  • Indirect prompt injection: if the agent reads external content (web pages, uploaded files, emails, RAG documents), that content can contain hidden instructions. Sandbox it, strip/escape it, and don't let retrieved text silently change the agent's behavior or trigger tool calls.
  • System prompt and developer instructions are not relied on as secrets — assume users can extract them.

Tool / function-calling guardrails (for agents that take actions)

  • The agent operates under least privilege — it can only call the specific tools and access the specific data it needs, scoped to the current user. It must not be able to read other users' data via a tool.
  • Dangerous actions require confirmation or are off-limits — sending money, deleting data, emailing arbitrary addresses, running code. Add human-in-the-loop for high-impact actions.
  • Tool inputs the model generates are validated like any other user input before execution (the model is effectively an untrusted user here).
  • If the agent executes code, it runs in a locked-down sandbox (no network, no filesystem, no secrets, resource/time limits) — never on your main server with your env vars in scope.
  • The agent can't be used as an SSRF proxy (fetching internal URLs on the attacker's behalf — see Section 10) or as a free passthrough to the raw LLM (your endpoint shouldn't just relay arbitrary prompts to the paid model with no scoping).

Cost & abuse control (this is how the "bored kid" runs up your bill)

  • Per-user token/request quotas and rate limits on every agent endpoint. One user must not be able to send thousands of requests and drain your OpenAI/Anthropic budget.
  • Max input length and max output tokens enforced per request.
  • Hard spend cap + alerting on your LLM provider account, plus a server-side circuit breaker that stops calling the model when a daily threshold is hit.
  • Conversation/context length is bounded so a user can't force ever-growing (and ever-more-expensive) context windows.

Content, privacy & output safety

  • Input and output moderation (provider moderation endpoint or your own filter) to stop the agent generating or amplifying harmful content that gets attributed to you.
  • The agent doesn't leak secrets, system internals, other users' data, or PII in its responses. Don't put secrets or cross-user data in the context window in the first place.
  • Log agent inputs, tool calls, and outputs (scrubbing PII) so you can audit abuse and debug incidents.
  • Outputs that get rendered in the browser are escaped/sanitized — an LLM can be induced to produce <script> or markdown that leads to XSS (see Section 9).
  • If you fine-tune or use RAG on user data, you've confirmed one user's data can't surface in another user's responses.

Fix pattern: Put a thin trusted layer around the model — authenticate the user, enforce quotas, scope tools and data to that user, validate everything the model emits before acting on it, and cap spend. The model is a powerful but untrusted component; your code stays in charge of permissions and money.


8. 💉 Input Validation & Injection

User input is hostile until proven otherwise. Client-side validation is for UX only — all validation must happen server-side.

  • All user input is validated server-side (type, length, format, range). Client-side checks don't count.
  • SQL injection: every query uses parameterized queries / prepared statements / ORM methods. Zero string concatenation, f-strings, .format(), or template literals building SQL with user input.
  • NoSQL injection (MongoDB etc.): user input isn't passed as raw query objects; operators like $where/$gt can't be injected via request params.
  • Command injection: user input never reaches exec, system, subprocess, child_process, eval unsanitized. Avoid shelling out with user data; if unavoidable, use arg arrays, never string interpolation.
  • Path traversal: file paths built from user input are validated/normalized so ../../etc/passwd can't escape the intended directory.
  • Open redirect: redirect targets from user input are validated against an allowlist — don't blindly redirect(req.query.next).
  • SSTI (server-side template injection): user input is never rendered as a template; it's passed as data to the template engine.
  • XXE: XML parsers have external-entity resolution disabled.
  • ReDoS: user input isn't fed to catastrophic-backtracking regexes; complex regexes are bounded/tested.
  • Request body limits + JSON depth limits prevent giant or deeply-nested payloads from exhausting memory.

9. 🧬 Output Encoding & XSS

Cross-site scripting lets an attacker run JavaScript in your users' browsers — stealing sessions, keystrokes, and data.

  • No dangerouslySetInnerHTML, v-html, or innerHTML with user-supplied content unless it's first sanitized with DOMPurify (or an equivalent allowlist sanitizer).
  • Where raw HTML rendering is genuinely required, it goes through DOMPurify every time.
  • Server-side template auto-escaping is enabled (the default in most engines — don't disable it).
  • User input reflected into HTML, attributes, JS, or URLs is context-appropriately encoded.
  • A Content-Security-Policy is set (Section 13) as a second line of defense — even if XSS slips through, CSP limits what injected script can do.
  • LLM-generated content rendered to users is treated as untrusted and sanitized too (see Section 7).
  • Test it: submit <script>alert('XSS')</script> and <img src=x onerror=alert('XSS')> into every field. It should render as plain text, never fire (Manual Test #12).

10. 🌐 SSRF (Server-Side Request Forgery)

If your app fetches URLs the user supplies (link previews, image proxies, "import from URL," webhook testers), an attacker can point it at internal addresses — including cloud metadata endpoints that hand out your credentials.

  • All private/internal IP ranges are blocked: 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 (cloud metadata!), ::1.
  • Only http and https schemes are allowed (no file://, gopher://, ftp://, etc.).
  • The hostname is resolved and the resulting IP is checked before the request is made — and re-checked to prevent DNS-rebinding (don't validate, then fetch a different IP).
  • Redirects are not blindly followed to internal addresses.
  • This applies to your AI agent too — if it can fetch URLs, it inherits all of these requirements (Section 7).
  • If your app fetches no user-supplied URLs, mark N/A and note why.

Test it: submit http://169.254.169.254/latest/meta-data/, http://127.0.0.1/, http://[::1]/. All must be rejected before any request fires (Manual Test #6).


11. 🎭 CSRF (Cross-Site Request Forgery)

CSRF tricks a logged-in user's browser into making state-changing requests they didn't intend (e.g. a hidden form on a malicious site that changes their email on your site).

  • Session cookies use SameSite=Lax or Strict (this alone blocks most CSRF), OR
  • All state-changing endpoints (POST/PUT/PATCH/DELETE) validate a CSRF token.
  • A cross-origin form POST to any state-changing endpoint fails (Manual Test #7).
  • State-changing actions are never exposed via GET requests.

12. 🔀 CORS

Misconfigured CORS lets malicious websites make authenticated requests to your API using your users' credentials.

  • CORS origin is an explicit allowlist of your actual domains — never *.
  • The origin is not dynamically reflected back from the request header (that's a wildcard in disguise).
  • credentials: true is never combined with a wildcard origin (the browser blocks it, but the intent is the bug).
  • Test it: a request with Origin: https://evil.com should not get an Access-Control-Allow-Origin echoing it back (Manual Test #9).

13. 🛡️ Security Headers

Free, browser-enforced protection that AI tools almost never set. Apply them via one global middleware, not per-route.

  • Content-Security-Policy — start with default-src 'self' and loosen as needed. Your strongest defense against XSS.
  • Strict-Transport-Security: max-age=31536000; includeSubDomains — forces HTTPS.
  • X-Frame-Options: DENY — stops clickjacking (your site embedded in a malicious iframe).
  • X-Content-Type-Options: nosniff — stops MIME-sniffing attacks.
  • Referrer-Policy: strict-origin-when-cross-origin — limits leaked referrer data.
  • All five are present on every response (Express: use helmet; Next.js: set them in next.config.js).
  • Verify with curl -I https://yourapp.com or at securityheaders.com (Manual Test #8).

14. 🚦 Rate Limiting & DoS Protection

Without rate limits, attackers brute-force logins, scrape data, and exhaust your resources for free.

  • Login, registration, and password-reset endpoints are rate limited (recommend ~10 attempts per 15 min per IP; return 429 when exceeded).
  • Expensive endpoints (search, exports, anything calling a paid downstream service or your LLM) are rate limited.
  • All API endpoints have a sensible default limit (Section 5).
  • X-Forwarded-For is not trusted for rate-limiting unless you're behind a known, trusted reverse proxy — otherwise attackers spoof it to bypass limits.
  • Edge-level DDoS protection is enabled (Cloudflare/WAF/CDN).
  • Rate limiting is enforced server-side / at the gateway, not in client code.
  • Test it: 50 rapid failed logins should start returning 429 (Manual Test #10).

15. 📎 File Uploads

Uploads are a classic foothold: a "JPG" that's actually a script, executed on your origin.

  • File type is validated by magic bytes (actual file content), not the filename extension.
  • Uploads are renamed to UUIDs server-side (no user-controlled filenames, no path traversal via filename).
  • Files are stored on a separate domain/bucket (S3, R2, GCS) — never served from your app's origin, so an uploaded file can't run as same-origin script.
  • Size limits are enforced server-side (not just a frontend attribute).
  • Images are re-encoded/processed server-side where feasible (strips embedded payloads and EXIF).
  • Uploaded files are scanned for malware where the risk warrants it.
  • Test it: upload a .jpg containing <script>alert('XSS')</script> and visit its URL — it must not execute on your origin (Manual Test #14).
  • No uploads? Mark N/A.

16. 💳 Payments & Webhooks (Stripe)

A webhook endpoint that trusts whatever hits it can be told "payment succeeded" by anyone with curl.

  • Stripe webhook signature is verified on every request with stripe.Webhook.constructEvent (or your language's equivalent). Invalid/missing signature → 400.
  • Webhook handlers are idempotent — processed event IDs are stored and duplicates are skipped (Stripe retries; double-processing = double-granting).
  • The full event lifecycle is handled, not just success: payment_intent.succeeded, invoice.payment_failed, customer.subscription.deleted, customer.subscription.past_due.
  • Entitlements (what a user gets) are driven by verified webhook events / server-side state, never by a client-side "payment worked" redirect.
  • Amounts and prices are set/validated server-side — the client can't submit its own price.
  • Test it: POST a fake event with no signature — it must return 400, not 200 (Manual Test #13).
  • No payments? Mark N/A.

17. 🤐 Error Handling & Information Disclosure

Verbose errors hand attackers a map: your DB schema, file paths, framework versions, and internal logic.

  • A global error handler catches all unhandled exceptions.
  • Production responses return generic messages only ({"error": "Something went wrong"}) — no stack traces, SQL errors, file paths, or library names.
  • Full error detail goes to server-side logs only.
  • Debug/development mode is OFF in production (Django DEBUG=False, Flask debug off, framework dev pages disabled).
  • Source maps are not deployed to production (they expose your original source).
  • Custom 404 / 403 / 500 pages are configured (no framework default error pages).
  • Errors return consistent JSON + appropriate HTTP status codes.
  • Test it: send invalid JSON, non-existent IDs, a lone ' — responses must stay generic (Manual Test #15).

18. 📦 Dependencies & Supply Chain

AI tools hallucinate package names. Attackers pre-register those exact fake names with malware ("slopsquatting"). Installing one gives an attacker code execution on your server.

  • Every dependency the AI suggested is verified to actually exist on the official registry (npm/PyPI/Packagist) with real download history and a maintained repo — before you install it.
  • Package names are checked for typosquatting (reqeusts vs requests, lodahs vs lodash).
  • Red flags investigated: < ~1,000 weekly downloads, published in the last 30 days, single unnamed maintainer, name suspiciously close to a popular package.
  • Exact versions are pinned in production (no ^ or ~).
  • Lock files are committed (package-lock.json, poetry.lock, yarn.lock).
  • npm audit / pip-audit shows no critical or high vulnerabilities (run it in CI).
  • Unused/unnecessary dependencies are removed (smaller attack surface; some things are built-in language features).
  • Automated dependency scanning is enabled (Dependabot / Renovate / Snyk).

19. 🏗️ Infrastructure, Deployment & Secrets Management

The app can be perfect and still fall to a misconfigured server or an over-privileged cloud key.

  • HTTPS/TLS everywhere, valid cert, HTTP → HTTPS redirect, HSTS on (Section 13).
  • Production secrets come from a secrets manager / host env vars, not a file in the repo (Section 1).
  • Separate environments for dev / staging / production with separate credentials — no shared keys, no prod data in dev.
  • Cloud IAM follows least privilege — services and keys can only touch what they need. No AdministratorAccess on app roles.
  • Storage buckets are private by default (the classic "public S3 bucket" leak); public access is explicit and intentional.
  • Admin panels, databases, and internal dashboards are not exposed to the public internet (VPN / IP allowlist / auth).
  • Default credentials on everything (databases, admin tools, services) are changed.
  • Server OS and runtime are patched / on supported versions.
  • CI/CD secrets are stored in the platform's secret store, and CI logs don't print them.
  • If you use containers, base images are scanned and kept updated.

20. 📊 Logging, Monitoring & Incident Response

You can't respond to what you can't see. Most breaches are detected late — or by the customer.

  • Security events are logged: failed logins, authz failures (403s), rate-limit hits, admin actions, key creation/revocation.
  • Logs never contain secrets, passwords, full tokens, or unmasked PII.
  • Alerts fire on anomalies: spikes in errors/429s/403s, spend spikes on metered services, logins from new geographies, bulk data access (Sections 5, 6).
  • Logs are centralized and retained (you can reconstruct what happened after an incident).
  • You have a basic incident-response plan: how to revoke/rotate every key, take the app offline, notify users, and preserve evidence.
  • Everything can be rotated quickly — DB credentials, API keys, signing secrets, OAuth secrets. Know how before you need to.
  • A security.txt / responsible-disclosure contact exists so researchers can report issues to you instead of dumping them publicly.

21. 🔐 Data Protection & Privacy

  • Data is encrypted in transit (TLS) and at rest (DB/storage-level encryption).
  • PII is minimized — you collect and retain only what you actually need.
  • Sensitive data has a retention + secure-deletion policy (and deletion actually deletes, including from backups over time).
  • Users can export and delete their data where required (GDPR/CCPA), and you honor it.
  • Audit logs record access to sensitive records.
  • Third parties you send data to are vetted; you're not leaking PII to analytics/LLM providers unintentionally.
  • Backups are encrypted and access-controlled (a leaked backup is a full breach).

📋 Copy-Paste AI Audit Prompts

Paste these into your AI coding agent one at a time. After each, tell it to: investigate every relevant file, report what's vulnerable with severity, fix it, then verify the fix. Don't batch — one category fully, or it skims.

Kickoff prompt: "Run a security audit on this project. Go through each of the prompts I give you one at a time. For each: investigate thoroughly (every file, route, config, migration, env file, frontend), write a report of what's vulnerable / safe / missing with severity (CRITICAL/HIGH/MEDIUM/LOW/PASS), make a fix plan, implement it, and verify against concrete goals. Put results in a security/ folder. Do not move on until the current category is fully done."

Prompt — Secrets exposure

Scan the entire codebase and git history for hardcoded secrets, API keys, and credentials. Find: strings matching key patterns (sk_, pk_, key_, token_, secret_, AKIA...), DB connection strings with embedded passwords, private keys/certs in code, .env files tracked by git, and any secret living in a client-exposed env var (NEXT_PUBLIC_*, VITE_*, REACT_APP_*). Confirm .env is in .gitignore and `git ls-files .env` is empty. For each finding: file, line, and the secure fix using a server-side env var. Flag any committed secret as needing rotation.

Prompt — Database access (RLS)

Audit database access control. For Supabase: list every table and confirm Row Level Security is enabled with explicit policies scoped to auth.uid() and a default-deny stance; flag any policy using USING(true) or FOR ALL without a WHERE condition. For Firebase: confirm rules require request.auth != null and scope to request.auth.uid. Confirm no service-role/admin DB key is used in frontend code. Show me which tables a request with only the anon/public key could read, and the fix for each.

Prompt — Authentication

Review authentication for vulnerabilities. Check: password hashing uses bcrypt/argon2/scrypt (cost >= 10) and never MD5/SHA-1/SHA-256; rate limiting exists on login/register/password-reset; JWT/session tokens expire (<= 24h access) and are invalidated server-side on logout; password reset links expire within 1h and are single-use; session cookies set httpOnly+secure+sameSite; MFA is available and required for admins; account enumeration is prevented (identical response/timing for existing vs non-existing emails on login/signup/reset). List each issue with severity and the exact code fix.

Prompt — Authorization / access control (IDOR)

Audit every API endpoint for authorization flaws (this is the highest-risk category). For each endpoint that takes a resource ID (in path, query, or body), confirm the handler verifies the authenticated user owns that resource (current_user.id == resource.owner_id) as a SEPARATE check from authentication, on BOTH reads and writes, returning 403 on failure. Check for horizontal escalation (User A reading User B's data by changing an ID) and vertical escalation (regular user hitting admin endpoints). Check for mass-assignment/over-posting (user setting fields like role, is_admin, balance, or user_id via the request body). List every endpoint, its current authorization status, and the fix.

Prompt — API abuse & rate limiting

Review the API for abuse resistance. For every endpoint check: is there per-user/per-key rate limiting and a usage quota; are there hard caps on result-set size with required pagination (no endpoint returns all rows); are request body sizes limited; can sequential IDs be enumerated to scrape data; for any metered downstream service (LLM, SMS, email, cloud), is there a spend cap and circuit breaker? Confirm X-Forwarded-For isn't trusted for rate limiting unless behind a trusted proxy. For GraphQL: check query depth/cost limits and disabled introspection in production. List gaps and fixes.

Prompt — AI / LLM agent security (if applicable)

If this project exposes an LLM/AI agent, audit it for AI-specific risks. Check: model output is never executed (no eval/SQL/shell) without validation; business logic and authorization live in code, not in the prompt; external/retrieved content (RAG, web, files, uploads) is treated as untrusted to prevent indirect prompt injection; tool/function calls are scoped to the current user with least privilege and dangerous actions require confirmation; any code execution is sandboxed with no secrets/network; per-user token and request quotas + max input/output length + an LLM spend cap and circuit breaker exist; input/output moderation is in place; the agent can't leak secrets, system prompts, PII, or other users' data; agent outputs rendered in the browser are sanitized. List each gap with severity and fix.

Prompt — Injection (SQL / command / path / etc.)

Audit all database queries and any use of system/exec/eval for injection. Find every query built with string concatenation, f-strings, .format(), or template literals containing user input and convert to parameterized queries/prepared statements. Check for NoSQL injection (Mongo operator injection), ORM raw-query misuse, command injection (user input reaching exec/subprocess/child_process), path traversal in file operations, open redirects, SSTI, and XXE. Show vulnerable code and the fixed version side by side.

Prompt — XSS / output encoding

Find every place user-supplied (or LLM-generated) content is rendered. Flag all uses of dangerouslySetInnerHTML, v-html, and innerHTML with unsanitized content and add DOMPurify where raw HTML is required. Confirm server-side template auto-escaping is on. Confirm a Content-Security-Policy exists as defense-in-depth. Show each risky render and its fix.

Prompt — SSRF

Find any code that fetches a URL derived from user input (link previews, image proxies, import-from-URL, webhook testers, or an AI agent's URL fetching). Confirm it blocks private IP 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), allows only http/https, and resolves+checks the IP before requesting (guarding against DNS rebinding). If none exists, say so. Show fixes for any gap.

Prompt — CSRF, CORS & security headers

Check three things. CSRF: are session cookies SameSite=Lax/Strict, or do all state-changing endpoints validate a CSRF token? CORS: is origin an explicit allowlist (never * or reflected), and is credentials:true never paired with a wildcard? Headers: are Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy set on every response via one global middleware (helmet / next.config.js)? List what's missing and the exact config.

Prompt — Anti-scraping & bot protection

Review the app's resistance to automated scraping. Check: is there site-wide per-IP and per-user rate limiting (not just on auth); do list/search/data endpoints cap page size and require pagination so bulk extraction is slow; is sensitive data gated behind auth and metered per account; are there CAPTCHA/challenge points on high-value flows; is there bot/WAF protection at the edge; are datacenter IP ranges challenged on sensitive endpoints; is there monitoring/alerting for scraping patterns (sequential ID walking, high-volume single-client access)? List what's missing and recommend concrete additions.

Prompt — Payments / webhooks (if applicable)

Audit Stripe (or other) payment handling. Confirm webhook signatures are verified on every request (invalid/missing -> 400); processed event IDs are stored for idempotency so duplicates are skipped; the full event lifecycle is handled (payment_intent.succeeded, invoice.payment_failed, customer.subscription.deleted, past_due); entitlements derive from verified server-side events, not a client redirect; and prices/amounts are validated server-side. List gaps and fixes.

Prompt — File uploads (if applicable)

Audit file upload handling. Confirm: file type is validated by magic bytes not extension; files are renamed to UUIDs server-side; files are stored on a separate domain/bucket (S3/R2/GCS), never the app origin; size limits are enforced server-side; images are re-encoded to strip payloads/EXIF. Show fixes for any gap.

Prompt — Error handling & deployment

Review error handling and production config for information disclosure. Confirm: a global error handler returns only generic messages to clients (no stack traces, SQL errors, file paths, or library names); full detail is logged server-side only; debug/dev mode is OFF in production; source maps are not deployed; custom 404/403/500 pages exist; errors return consistent JSON with correct status codes. Also confirm HTTPS with HSTS and that no secrets are hardcoded. Show every place internals could leak and the secure pattern for this framework.

Prompt — Dependencies & supply chain

Audit all dependencies (package.json / requirements.txt / pyproject.toml / composer.json). For each: confirm it exists on the official registry with real download history and a maintained repo; check for typosquatting and slopsquatting (names close to popular packages, suspiciously new packages, < ~1000 weekly downloads, single unnamed maintainer); confirm exact versions are pinned and lock files committed; run npm audit / pip-audit and report critical/high issues; flag unused or unnecessary dependencies. List anything suspicious.

🧪 Manual Penetration Tests (do these yourself)

These verify the things AI can't confirm from inside the code — your live app's actual behavior. Do them before real users arrive. If you only do five, do tests 15.

1. Database isn't publicly queryable

Hit your REST endpoint with only your public/anon key (both are visible in your frontend):

curl "https://YOUR_PROJECT.supabase.co/rest/v1/users?select=*" -H "apikey: YOUR_ANON_KEY"

PASS: empty array or permission error. FAIL: returns real user data. (Firebase: try reading a collection while unauthenticated.)

2. Protected routes reject unauthenticated requests

In DevTools → Network, copy a request to a protected endpoint while logged in. Log out. Replay it without the session cookie/auth header. PASS: 401. FAIL: returns data. Also: call an admin-only endpoint with a regular user session → should be 403.

3. No secrets in git

git ls-files .env                 # PASS: no output
grep "\.env" .gitignore           # PASS: shows a match
gitleaks detect --source . --verbose   # PASS: no leaks (scans history too)

4. Can't access another user's data by changing an ID

Create User A and User B. As User A, request a resource that belongs to User B:

GET  /api/resources/{user_b_id}    -> should return 403
PUT  /api/resources/{user_b_id}    -> should return 403

Test every endpoint that takes a user-scoped ID (documents, orders, payments, profile). FAIL: you get B's data.

5. No secret keys in the browser

DevTools → Sources → search all files for sk_, AKIA, Bearer, secret, private_key. Then Network tab → watch for secret tokens sent from the client to third-party APIs. PASS: nothing. FAIL: any match.

6. Internal URLs are blocked (SSRF)

In any feature that fetches a user-supplied URL, submit:

http://127.0.0.1/        http://localhost/        http://[::1]/
http://10.0.0.1/         http://169.254.169.254/latest/meta-data/

PASS: all rejected before any request. FAIL: any returns content. (Skip if no URL-fetching feature.)

7. Cross-origin form POSTs are blocked (CSRF)

While logged into your app, open an HTML file containing a hidden auto-submitting form that POSTs to one of your state-changing endpoints (e.g. change-email) from a different origin. PASS: action fails (403 / no effect). FAIL: it executes.

8. Security headers are present

curl -I https://yourapp.com | grep -i "content-security-policy\|strict-transport\|x-frame-options\|x-content-type"

PASS: all present. Or check securityheaders.com.

9. CORS isn't wide open

curl -I -H "Origin: https://evil.com" https://yourapp.com/api/anything | grep -i "access-control-allow-origin"

PASS: no header or your domain only. FAIL: * or echoes https://evil.com.

10. Login can't be brute-forced

for i in $(seq 1 50); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yourapp.com/api/login \
    -H "Content-Type: application/json" -d '{"email":"test@test.com","password":"wrong"}'
done

PASS: starts returning 429 after a few. FAIL: all 50 return 401 unthrottled.

11. SQL injection doesn't work

In login, search, and filter inputs submit: ' OR '1'='1 and '; DROP TABLE users; -- PASS: no unexpected data, no DB error leaked. FAIL: unexpected data or a SQL error appears.

12. XSS doesn't execute

In every text input and URL param submit: <script>alert('XSS')</script> and <img src=x onerror=alert('XSS')> PASS: shown as plain text. FAIL: an alert fires.

13. Stripe webhooks reject fakes

curl -X POST https://yourapp.com/api/webhook -H "Content-Type: application/json" \
  -d '{"type":"payment_intent.succeeded","data":{"object":{"id":"pi_fake"}}}'

PASS: 400 / signature error. FAIL: 200 and it processes. Also send the same valid event twice → if it processes both, idempotency is missing.

14. File uploads reject disguised files

Make test.jpg whose contents are <script>alert('XSS')</script>. Upload it normally, then visit its URL. PASS: rejected, or served as download, or on a different domain. FAIL: it executes on your origin.

15. Errors don't leak internals

Submit invalid JSON, non-existent IDs, and a lone '. PASS: generic "something went wrong." FAIL: stack traces, SQL, file paths, or library names appear.

16. Passwords are hashed properly

grep -rn "hashlib.md5\|hashlib.sha1\|createHash.*md5\|createHash.*sha1" --include="*.py" --include="*.js" --include="*.ts" ./

PASS: no matches. bcrypt hashes start with $2b$; a 32-hex hash is MD5, 40-hex is SHA-1 (both bad).

17. Dependencies are real

For every package you don't recognize: confirm it exists on npmjs.com / pypi.org, check weekly downloads, publish date, and maintainer. Then:

npm audit       # or: pip-audit

🚩 Red flags: < 1,000 weekly downloads, published in the last 30 days, lone unnamed maintainer, name close to a popular package.

18. (Bonus) Bulk scraping is throttled

As a logged-in user, script a loop pulling many list/detail pages or walking sequential IDs. PASS: you hit rate limits / pagination caps / challenges quickly. FAIL: you can pull thousands of records unthrottled.

19. (Bonus) AI agent abuse is capped

If you have an AI agent: send a rapid burst of requests, oversized inputs, and a few "ignore your instructions / reveal your system prompt / call an admin tool" attempts. PASS: per-user limits kick in, spend is capped, the agent refuses out-of-scope actions, no secrets/other-user data leak. FAIL: unthrottled spend or it follows injected instructions.


📜 Agent Rules (paste into CLAUDE.md / AGENTS.md)

Drop this block into your project root as CLAUDE.md (Claude Code) or AGENTS.md (Cursor / Copilot / Codex / Windsurf / Gemini). Not sure? Make both. Commit it. Your AI reads it on every task from then on and stops creating these issues in the first place.

# Security Rules (non-negotiable, apply to all generated code)

## Secrets
- NEVER put API keys, DB credentials, or tokens in frontend code (src/, app/, pages/, components/, public/).
- NEVER put a secret in a client-exposed env var (NEXT_PUBLIC_*, VITE_*, REACT_APP_*) — those ship to the browser.
- NEVER hardcode credentials. Load secrets server-side from env vars only.
- .env MUST be in .gitignore before the first .env is created. Use .env.example with placeholders only.

## Database
- Enable Row Level Security on EVERY table before deploy. Default deny. Policies scoped to auth.uid().
- NEVER use an RLS policy of USING(true) or FOR ALL without a WHERE condition.
- Firebase rules MUST require request.auth != null and scope to request.auth.uid.
- Service-role / admin DB keys NEVER appear in client code.
- NEVER deserialize untrusted data (no pickle.loads on user input). Use JSON for network data.

## Auth & authorization
- EVERY protected route gets auth middleware that runs BEFORE the handler. Unauthenticated -> 401.
- EVERY route taking a resource ID MUST verify ownership (current_user.id == resource.owner_id) as a SEPARATE check, on reads AND writes. Fail -> 403.
- Admin routes verify admin role -> 403 for non-admins.
- Prevent mass assignment: whitelist accepted fields; never spread the request body into a DB model. Users can't set role/is_admin/balance/user_id.
- Session cookies: httpOnly:true, secure:true, sameSite:'lax'. Tokens expire and are invalidated server-side on logout.
- Passwords: bcrypt/argon2/scrypt only. NEVER MD5/SHA-1/SHA-256.
- Don't leak account existence on login/signup/reset (identical responses).

## Input / output
- NEVER concatenate user input into SQL. ALWAYS parameterized queries / ORM methods.
- NEVER use dangerouslySetInnerHTML/v-html/innerHTML with unsanitized content. Sanitize with DOMPurify.
- ALL validation server-side (client-side is UX only). Validate type/length/format/range.
- No command injection (no user input into exec/subprocess/eval), no path traversal, no open redirects, no SSTI/XXE.
- Enforce request body size limits.

## URL fetching (SSRF)
- If fetching user-supplied URLs: block 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; resolve and check the IP BEFORE the request.

## API & abuse
- Per-user/per-key rate limits + quotas on ALL endpoints, not just auth.
- List/search endpoints: max page size + required pagination. No endpoint returns all rows.
- Spend caps + circuit breakers on every metered downstream service (LLM, SMS, email, cloud).
- Don't trust X-Forwarded-For for rate limiting unless behind a trusted proxy.

## AI / LLM agent (if present)
- Treat model output as untrusted: never eval/SQL/shell it without validation.
- Keep authorization and business logic in code, not the prompt. Assume users can extract the system prompt.
- Treat retrieved/external content as untrusted (indirect prompt injection). Scope tools to the current user with least privilege. Dangerous actions need confirmation. Sandbox any code execution (no secrets/network).
- Enforce per-user token/request quotas, max input/output length, and an LLM spend cap. Moderate input and output. Sanitize agent output rendered in the browser.

## Headers / CORS / CSRF
- Set CSP, Strict-Transport-Security, X-Frame-Options:DENY, X-Content-Type-Options:nosniff, Referrer-Policy on ALL responses via one global middleware (helmet / next.config.js).
- CORS origin = explicit allowlist. NEVER '*', never reflected, never wildcard + credentials:true.
- Session cookies SameSite=Lax/Strict OR CSRF tokens on all state-changing endpoints.

## Files / payments / errors / deps
- Uploads: validate by magic bytes, rename to UUID, store on a separate bucket/domain, enforce size server-side.
- Stripe webhooks: verify signature every request (-> 400 if bad), idempotent via stored event IDs, handle failure events. Entitlements from verified server-side events, not client redirects.
- Errors: global handler, generic client messages only, full detail to server logs, debug OFF in production, no source maps in prod.
- Dependencies: verify each package exists on the official registry (guard against typo/slopsquatting) BEFORE installing. Pin exact versions. Commit lock files.

🎯 Where to start (priority order)

You don't have to do everything at once. In order:

  1. TIER 0 (the five). Today. These are what get apps breached.
  2. Sections 14 — secrets, database, auth, access control. The foundation.
  3. Sections 57 — API abuse, anti-scraping, AI agent. Your three stated concerns.
  4. Sections 818 — injection, XSS, SSRF, CSRF, CORS, headers, rate limiting, uploads, payments, errors, dependencies.
  5. Sections 1921 — infrastructure, monitoring, data protection. The operational layer that catches what code review misses.
  6. Run the manual tests against your live app. Then re-run after every significant change.

🧠 Final word

This checklist closes the gap between "the AI made it work" and "the AI made it safe" — and it covers the two things off-the-shelf checklists skip: scraping and API/cost abuse, plus the AI-agent attack surface if your product is an agent.

But repeat after me: there is no unhackable. What you've built here is a high wall with cameras — enough to send casual attackers and bored kids looking for an easier target, and enough to detect and slow down the serious ones. When you have real users and real revenue, hire a penetration tester. A human actively trying to break your app will find things no checklist can, and that's worth far more than the cost.

Ship it locked down. Then keep watching the logs.


Built by merging the vibe-check ruleset (MIT) and Sherlock Forensics' security prompts, expanded with anti-scraping, API-abuse, AI/LLM-agent, infrastructure, and monitoring coverage. Sources documented real breaches across 5,600+ scanned vibe-coded apps. This is a strong first pass — not a substitute for a professional audit.