Bonbon Dino — Security Checklist¶
These are best practices and references, not hard requirements. They are the pre-launch security review for any client site — the things that, skipped, turn into a 2am incident or a surprise cloud bill. Most are 2–10 minutes of work. Adapt or skip based on what the project actually does, but treat this as the gate before handing a site to a client.
The principles are cloud-provider-agnostic: they apply whether the data lives in Cloudflare D1 / Turso, AWS, Supabase, or anywhere else. Where this agency's default stack (Cloudflare Workers + Turso/D1 + Drizzle + Next.js) has a concrete implementation, the section links to the file that covers it in detail rather than repeating it.
Companion files¶
These siblings live next to this file. Several of them carry the detailed implementation for a point below — follow the cross-references instead of re-solving:
website-checklist.md— CSP (dev vs prod), security headers, frontend patternsadmin-panel-checklist.md— JWT sessions, password hashing, centralized auth middlewarecontact-form-checklist.md— Turnstile, server-side validation, secrets, error handlingdb-and-deploy-checklist.md— WAF rate limiting, Wrangler secrets,NEXT_PUBLIC_*build-time varsemail-checklist.md— Resend API key handling, transactional emaildebugging-lessons.md— symptom → root cause → fix log, including auth/forms gotchas
1. Protect Yourself, Not Just the App¶
- What it does: Covers the legal and operational exposure that starts the moment the site collects any user data (a contact form is enough).
- Why: Data-collection puts the project under privacy law (GDPR, CCPA, and local equivalents). "We didn't know" is not a defense, and the liability is the agency's and the client's, not the framework's.
- How to implement:
- Ship a privacy policy before go-live on any site with a form or analytics.
- Know where user data physically lives — which provider, which region. For the default stack
that's the Turso/D1 region; see
db-and-deploy-checklist.md. Write it down in the handover. - Collect the minimum. Data you never store can't leak.
2. Authorize at the Data / Server Layer, Never the Client¶
- What it does: Ensures access control is enforced where the attacker can't reach it — the server or the database — not in the browser.
- Why: Anything enforced only in client code can be bypassed with DevTools. If your data store exposes a client-facing API, a missing authorization rule means anyone can read the whole database. This is the single most common "naked app" failure.
- How to implement:
- If the database has a public/client API (Supabase, Firebase, any "query from the browser" setup): enable row-level authorization. On Supabase that's Row Level Security (Auth → Policies); zero policies means the table is world-readable. AWS is IAM / fine-grained access. Default-deny, then open the minimum.
- On this stack there is no public DB API — the DB is reached only through server-side routes.
The equivalent rule is: every API route and server action checks auth before touching data.
Use the centralized middleware gate, don't hand-roll per-route — see
admin-panel-checklist.md §11. - Either way, ask: "If someone calls this endpoint directly with a forged/empty session, what do they get back?" The answer must be "nothing."
3. Test the Failure Path, Not Just the Happy Path¶
- What it does: Surfaces the auth and form bugs that only appear when a user does the wrong thing.
- Why: The happy path almost always works; ~80% of real auth bugs live in the error branches.
- How to implement: Before launch, manually run:
- Wrong password 5+ times in a row (lockout / rate-limit behavior).
- Password reset for an email that doesn't exist (must not reveal whether the account exists — see §11).
- Verification or reset link clicked twice (single-use? expired cleanly?).
- Signup / form submit with an already-existing email.
- A second form submit after a failed one (Turnstile token reuse — this is a known trap; see the
Turnstile-token entry in
debugging-lessons.md). - Login flow and reset flow details live in
admin-panel-checklist.md §4and§5.
4. Run a 2-Minute Security Baseline¶
- What it does: A fast AI-assisted pass for missing security headers and obvious posture gaps.
- Why: Security headers (HSTS, X-Content-Type-Options, a real CSP) are cheap and catch a whole class of attacks, but they're easy to forget on a new project.
- How to implement:
- Prompt your AI: "Review my app as a security specialist and make sure I have strong security headers and a solid baseline security posture."
- Confirm the CSP is environment-split: dev CSP may include
unsafe-eval, production must not — seewebsite-checklist.md.
5. Run an OWASP Review¶
- What it does: Targets the OWASP Top 10 — where SQL injection, XSS, and broken-auth bugs actually get caught.
- Why: These are the highest-impact, most-exploited vulnerability classes. A focused pass finds them before an attacker does.
- How to implement:
- Prompt your AI: "Review my app against OWASP standards and highlight vulnerabilities."
- The
security-reviewskill (andoh-my-claudecode:security-revieweragent) can run this more rigorously than an ad-hoc prompt — use it on anything handling auth or payments.
6. Client-Side Validation Is UX, Not Security¶
- What it does: Draws the line between validation for user convenience and validation for safety.
- Why: Attackers disable JS and hit the API directly. Any check that only runs in the browser is decorative from a security standpoint.
- How to implement:
- Keep client-side validation for fast feedback, but re-validate every field on the server, every
time. Use one shared schema so the rules can't drift — see
contact-form-checklist.md §1(schema/validation) and§4(backend endpoint).
7. Know the Three Places AI-Generated Code Leaks Data¶
- What it does: Catches the leaks that generated code introduces most often.
- Why: The three recurring offenders are:
.envvalues bundled into the frontend, API responses returning more fields than the UI needs, and secrets printed into logs. - How to implement:
- Prompt your AI: "Check my app for credential or sensitive data leaks in frontend or API routes."
- Confirm only
NEXT_PUBLIC_*(intentionally public) vars reach the bundle —db-and-deploy-checklist.md §6. - Confirm runtime secrets are Wrangler secrets, not committed or build-time —
db-and-deploy-checklist.md §7,contact-form-checklist.md §9. - Trim API responses to the fields the client actually uses; never return a whole user row.
8. No Secret API Keys in the Frontend¶
- What it does: Keeps credentials that grant real access out of the browser.
- Why: Anything shipped to the browser is readable by anyone. If a secret key is in the bundle, assume it's already compromised.
- How to implement:
- Move secret keys server-side, or proxy the third-party call through your own API route.
- Know the difference between a public key meant to ship (e.g. the Turnstile site key) and a secret key that must never (the Turnstile secret key, Resend API key, DB tokens). When in doubt, treat it as secret.
9. Rate-Limit Before Someone Burns Your API Bill¶
- What it does: Caps how fast any endpoint that calls a paid service can be hit.
- Why: An uncapped endpoint in front of a metered API is a financial DoS waiting to happen — a bill can jump from $20 to $200 in a day from a single script.
- How to implement:
- Rate-limit every endpoint that triggers a paid call (email send, LLM, SMS, payment, third-party
lookups). Provider-agnostic; on this stack use Cloudflare WAF rate limiting on auth and
form-submit endpoints — see
db-and-deploy-checklist.md §11. - Set a spend alert/budget cap at the provider so you find out from an email, not an invoice.
10. CAPTCHA on Public Forms + CORS Locked to Your Domain¶
- What it does: Stops automated bot floods against anything a stranger can submit.
- Why: Public forms and public endpoints get scraped and hammered by bots within days of launch. Two cheap controls kill most of it.
- How to implement:
- Add a CAPTCHA to every public form — Cloudflare Turnstile is free and already the agency default;
see
contact-form-checklist.md §5and§2a. For staging on*.workers.dev, register the hostname —db-and-deploy-checklist.md §18. - Lock CORS to the site's own domain(s); don't ship
Access-Control-Allow-Origin: *on endpoints that mutate data or cost money.
11. Error Messages That Don't Leak¶
- What it does: Separates what the user sees from what gets logged.
- Why: Detailed errors ("SELECT * FROM users failed", stack traces, "no account with that email") hand attackers a map of your schema and your user list.
- How to implement:
- Show users a generic message ("Something went wrong, try again", "If that email exists we sent a link"). Log the full error server-side where only you can read it.
- Don't reveal account existence in auth flows (see §3). Watch for the inverse failure too: errors
swallowed so silently that real problems hide — see the fire-and-forget email entry in
contact-form-checklist.md §12g.
Quick Checklist — "Is the security baseline done?"¶
- [ ] Privacy policy live; data location (provider + region) recorded in the handover
- [ ] Every API route / server action checks auth before touching data (or DB row-level rules are default-deny)
- [ ] Failure paths tested: wrong password ×5, reset for unknown email, link clicked twice, duplicate signup, retry after failed submit
- [ ] Security headers present; production CSP has no
unsafe-eval - [ ] OWASP pass run (SQLi / XSS / broken auth) via prompt or
security-reviewskill - [ ] All form input re-validated server-side with the shared schema
- [ ] Leak check run: no secrets in the bundle, API responses trimmed, no secrets in logs
- [ ] No secret keys in the frontend; only public keys ship to the browser
- [ ] Every paid-API endpoint is rate-limited; provider spend alert set
- [ ] CAPTCHA on all public forms; CORS locked to the site domain
- [ ] User-facing errors are generic; full errors logged server-side; auth flows don't reveal account existence
How to extend this file¶
New security lesson from a project? Add or rewrite a numbered section in the What it does / Why /
How to implement house style. Keep it cloud-provider-agnostic — state the principle first, then
link to the companion file that holds the stack-specific implementation rather than duplicating it.
No client names or hardcoded values in section bodies (see RETROSPECTIVE.md Hard Rules). When the
lesson is really a bug with a non-obvious root cause, it belongs in debugging-lessons.md instead;
cross-link to it from here.