steelforesight/SECURITY-CHECKLIST.md

707 lines
54 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔒 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](#-agent-rules-paste-into-claudemd--agentsmd)** 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](#-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](#-manual-penetration-tests-do-these-yourself)**. 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](#7--ai--llm-agent-security-the-part-everyone-forgets)**. 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):
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
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:
```bash
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.
```markdown
# 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.*