Security posture
A sourdough starter for security defaults. The seven things that account for most of what bites vibe-coded apps in their first six months. Fork into your project's CLAUDE.md or keep as a separate
SECURITY.mdyour AI re-reads on every session.
1. Secrets
Rule: No secret in any file that touches git. Ever.
.env.localis gitignored — use it for development.- Production secrets live in a secrets manager: Supabase Vault if you're on Supabase, Doppler / Infisical / AWS Secrets Manager / Vercel env vars otherwise. Not committed files.
- Never paste a production secret into a chat with an AI. Treat the AI as you'd treat a public log: if it went in, assume it's been recorded somewhere.
NEXT_PUBLIC_prefix exposes the variable to the browser. Don't put a service-role key, an admin token, or any server-only credential behindNEXT_PUBLIC_. The pattern misleads the AI; the AI follows the pattern; the key is now on the client.- Rotate when in doubt. If a secret might have leaked (chat, screenshot, log dump), rotate immediately. Faster than auditing whether it actually leaked.
Tool: secret_leak_finder scans 29 distinct credential patterns. Run before every deploy.
2. Row-Level Security (Supabase)
Rule: RLS enabled on every table. Every policy scoped to auth.uid().
- Default-deny: turn RLS on, then write explicit allow policies.
USING (true)is not a policy. It's the absence of a policy with extra steps. Catches everyone, lets everyone through.auth.role() = 'authenticated'is not the same asauth.uid() = user_id. The first lets any logged-in user through; only the second scopes to the owner. The AI confuses these.WITH CHECKon INSERT / UPDATE.USINGonly covers SELECT-shaped reads. WithoutWITH CHECK, a user can update a row to reassign it to another user — the row-reassignment bug.SECURITY DEFINERfunctions bypass RLS. Use sparingly; when you use one, guard inside the function (checkauth.uid()explicitly).- System tables (
spatial_ref_sys,pg_*) can't have RLS toggled — that's normal. Don't fight it.
Tool: rls_checker scans for the 8 canonical RLS misconfigurations. Run before every migration.
3. Auth check placement
Rule: Every protected route has a detectable auth check. Centralised if possible.
- Middleware is the cleanest place — one auth check covers a whole path prefix (
/dashboard/*,/api/admin/*). Audit surface is one file. - Per-route checks are fine but get audited per-route, which means missed-on-new-route is a likely bug class.
- Routes the AI tends to leave unprotected:
/admin,/api/admin,/api/internal, anything under/dashboardthat was added after the middleware was written. - Webhooks need signature verification. Stripe (
stripe.webhooks.constructEvent), Svix, custom HMAC — pick one, use it on every webhook handler. Without verification, anyone canPOSTto your endpoint claiming to be Stripe.
Tool: auth_flow_trace flags protected-shaped routes without detectable auth gating, plus webhooks without signature verification.
4. Input validation parity
Rule: The server validates everything the client validates, plus everything the client doesn't.
- Client validation is UX. Server validation is security. Never mistake one for the other.
<input required>is a client hint, not a check. The server still has to validate.- Schema library on both sides (Zod / Yup / Valibot etc.) — use the same library and ideally share the schema object so they can't drift.
- The "client validates, server trusts" bug is the most common shape: form looks right, behaves right, then someone
curls the endpoint with garbage and the database accepts it.
Tool: form_validation_audit flags client/server validation mismatches.
5. CORS
Rule: No wildcard origin in production. Ever.
- Wildcard origin (
Access-Control-Allow-Origin: *) lets any site's JavaScript hit your API. - Wildcard origin +
credentials: true— the browser blocks the request, but the intent reveals an architectural flaw. Don't ship the combination even though it doesn't "work." - Reflective origin echoing (returning whatever the request's
Originheader was) is wildcard with extra steps. - Allowlist specific domains. If you need many, build the allowlist explicitly; don't write a regex that drifts.
Tool: cors_audit covers the 6 CORS patterns.
6. Webhook signature verification
Rule: Every webhook handler verifies the signature before doing anything.
- Stripe:
stripe.webhooks.constructEvent(rawBody, sig, webhookSecret). Raw body, not parsed JSON — parsing changes the bytes and the signature fails. - Supabase Auth webhooks: HMAC SHA-256 of the body with the webhook secret.
- Svix / Hookdeck / similar: provider-specific verification SDK.
- Idempotency on every handler. Webhook providers can replay, can fire out of order, can deliver duplicates. Every handler needs to be safe to receive the same event twice.
The pattern: verify, then dispatch. If verification fails, return 401 silently — don't echo why.
7. Prompt injection (if you ship anything AI-shaped)
Rule: Treat fetched content as adversarial.
- Anything your app fetches and forwards into a prompt (email body, scraped page, tool response, user-submitted document) can contain injection payloads — instructions to ignore the system prompt, role-breakout attempts, capability exploits.
- Severity scales with where the content came from. A user-typed message is one thing; a scraped third-party page is much worse.
- Don't put untrusted content into a system prompt position. If you need to summarise an email, put the email inside a
<email_content>tag (or similar) and instruct the model that anything inside that tag is data, not instructions. - The model won't always follow that instruction. Defense-in-depth: don't give the model tools it shouldn't use on untrusted content.
Tool: prompt_injection_scan covers 6 injection categories with content-type-aware severity.
How to feed this starter
Add entries when:
- A new failure mode shows up in your project. ("Our webhook handler trusted the body before verifying — fixed by reordering, lesson: verify-then-dispatch.")
- A new tool / library introduces its own security pattern. ("Clerk webhooks use Svix signature verification, not the Stripe pattern.")
- The AI suggests a workaround that would have shipped a hole. ("AI suggested disabling RLS to fix a 'permission denied' error — actually fix is the WITH CHECK policy. Recorded so it doesn't suggest it again.")
Remove entries when:
- The underlying stack moves and the gotcha is no longer real.
- A platform fixes the default (e.g. if Supabase ever defaults RLS to on, the "always enable RLS" rule becomes redundant).
Companion tools (Fizzgig)
The _audit tools that map to each section above:
| Section | Tool |
|---|---|
| 1. Secrets | fizzgig__secret_leak_finder (29 credential patterns) + fizzgig__env_auditor (.env file structure) |
| 2. RLS | fizzgig__rls_checker (8 misconfiguration patterns) |
| 3. Auth + webhooks | fizzgig__auth_flow_trace |
| 4. Validation | fizzgig__form_validation_audit |
| 5. CORS | fizzgig__cors_audit |
| 7. Prompt injection | fizzgig__prompt_injection_scan |
All of these collapse into a single fizzgig__audit MCP tool at the June 2026 Audit Suite launch — one MCP tool, all checks, one verdict.
This is a sourdough starter — fed as the project evolves and as the security landscape moves. Last meaningful update: 2026-05-28.