Skip to main content
All posts
vibe-codingsecurity-auditchecklistpre-launchpractical-guide

Vibe-Coded App Security Audit Checklist: Step-by-Step Before Launch

April 15, 202614 min read

This is the audit we run on every app before launch. It is structured as a sequence — you go through the steps in order, fix what fails, and move on. Do not skip steps. Do not reorder. Each step is there because skipping it in a previous audit resulted in something specific getting exploited.

Allow 60-90 minutes for a complete pass. An experienced developer can clear most of it in 40. If you want the automated version, VibeArmor runs all 120 checks in three minutes — this guide is the manual equivalent for people who want to understand what the scanner is doing and why.

Pre-flight: know what you are auditing

Before touching the app, write down the following. The audit is meaningless without this context.

  • The live URL. Not localhost. Not a preview you will delete. The actual URL users will hit.
  • The database (Supabase / Firebase / PlanetScale / other).
  • The authentication provider (Supabase Auth / Clerk / NextAuth / custom).
  • The payment processor, if any.
  • Every third-party API the app calls (OpenAI, Stripe, SendGrid, etc.).
  • The AI coding tool used (Cursor, Lovable, Bolt, Claude Code). This matters because the failure patterns differ slightly per tool.

Phase 1: Secrets audit (10 minutes)

This phase is the highest-leverage checklist in existence. A single exposed key undoes every other protection in the app. Always do it first.

Step 1. Open DevTools on the live site. Go to the Sources tab.

Search for each of the following strings in the JavaScript bundle: sk_live, sk_test, service_role, SUPABASE_SERVICE, OPENAI_API, SENDGRID, STRIPE, PRIVATE, SECRET. Each match is a finding. The scanner performs this exact search across the full bundle including dynamically imported chunks.

Step 2. Check the .env file against the repository.

Run git log --all -S "sk_live" and the same for each key you found in Step 1. If any key was ever committed — even accidentally, even on a deleted branch — assume it is compromised. GitHub secret scanners find these within minutes of a push.

Step 3. Rotate any exposed key.

If Step 1 or 2 found anything, rotate the key before proceeding. For Supabase, generate a new service role key and delete the old one. For Stripe, roll the secret key. For OpenAI, revoke and re-issue. Update the live deployment's environment variables, not the old .env, not a Git commit.

Step 4. Refactor calls to server-side.

Every call that needs a sensitive key moves to a server-side API route. The client never sees the key. The client calls your API route, which calls the third-party service with the server-only key. Strip every NEXT_PUBLIC_ prefix from any variable holding a secret.

Phase 2: Database access audit (15 minutes)

If the app uses Supabase, Firebase, or any database with client-side SDKs, this phase matters more than any other. It is where the worst breaches happen and where AI-generated code consistently gets it wrong.

Step 5. List every table in the database.

For Supabase, open the Database > Tables view. For Firebase, list your Firestore collections. Write them all down.

Step 6. Enable Row-Level Security on every table.

Run the Supabase SQL query SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' and cross-check against your list. Every table in your app must have RLS enabled. ALTER TABLE {name} ENABLE ROW LEVEL SECURITY; for any table that does not.

Step 7. Test every table with the anon key.

Open the browser console on a page that already loads the Supabase client, then manually run:

for (const table of ['profiles', 'orders', 'messages' /* ...your list */]) {
  const { data, error } = await supabase.from(table).select('*').limit(5);
  console.log(table, data, error);
}

For each table, one of three things should happen. (a) You get back only your own rows — good, RLS is working. (b) You get back an empty array — probably good, means the anon role has no read access at all. (c) You get back rows for other users — broken RLS, fix immediately.

Step 8. Audit every policy for USING(true).

In Supabase Studio, go to Authentication > Policies. Scroll every policy on every table. If any of them has USING(true) without an explicit role restriction, it grants access to every role including anon. Replace with a specific condition like USING(auth.uid() = user_id) for authenticated access, or TO service_role USING(true) for service-role-only access.

Step 9. Confirm every UPDATE policy has a WITH CHECK.

An UPDATE policy with only USING validates which rows the user can modify but not what values they can set. Users can change their own role column from user to admin. Every UPDATE policy needs a WITH CHECK clause that validates the new values — typically the same expression as USING plus any field-level restrictions.

Phase 3: Authentication and authorization audit (20 minutes)

Step 10. Visit every admin route in an incognito window.

List every page path in the app that starts with /admin, /dashboard, /settings, /internal, or similar. Open each in incognito (not signed in). Any page that loads without redirecting to login is a broken access control. Fix by adding server-side auth middleware — in Next.js, a middleware.ts that checks the session before serving protected routes.

Step 11. Call every protected API endpoint with no token.

Open a fresh Postman/Hoppscotch session. For every API route (/api/*), make a request without an Authorization header or session cookie. Any 200 response on a sensitive endpoint is a finding. Expected behavior: 401 Unauthorized.

Step 12. Test for IDOR on every ID-based endpoint.

Log in as User A. Find any API route that takes an ID in the path or body — /api/users/123, /api/orders/abc, /api/messages/xyz. Change the ID to one belonging to User B and re-send. If you receive User B's data, the endpoint is missing an ownership check. Every such endpoint needs the two-line server-side guard: verify the authenticated user owns or has permission to access the requested resource.

Step 13. Inspect login form for hidden fields.

View source on the login page. Look for hidden inputs with names like isAdmin, role, userType, isPremium, verified. If any exist and the server reads them into auth or authorization logic, you have the XBEN-052 pattern — an attacker sets the value client-side and bypasses auth. All identity and role data comes from the verified session, never from the request body.

Step 14. Test password reset for email-based user takeover.

Request a password reset for a test account. Inspect the reset link. If it contains only an email address or user ID with no cryptographic token, or if the token is predictable (sequential, timestamp-based), an attacker can generate valid reset links for any email. Tokens must be random, single-use, and expire within one hour.

Phase 4: Rate limiting and abuse protection (10 minutes)

Step 15. Fire 100 login attempts in 60 seconds.

Script a loop that POSTs to /api/auth/login 100 times with random credentials. All 100 should not succeed in reaching the server — after 5-10 attempts, responses should return 429 Too Many Requests. If all 100 go through, the login endpoint has no rate limit. Fix with Upstash Ratelimit, Vercel's built-in limiter, or Cloudflare.

Step 16. Check rate limits on expensive endpoints.

Repeat Step 15 for every endpoint that calls a paid API: /api/chat (OpenAI), /api/send-email (SendGrid), /api/sms (Twilio), /api/generate-image (any image API). An unprotected endpoint here means an attacker can run up your bill by the thousands overnight.

Step 17. CAPTCHA on signup.

If the app allows account creation, bots will create accounts in bulk. This poisons analytics, consumes free-tier resources, and gives attackers a surface to scan from “legitimate” user accounts. Cloudflare Turnstile is free and takes 15 minutes to add.

Phase 5: Transport and headers audit (10 minutes)

Step 18. Verify HTTPS enforcement.

Visit the http:// version of your domain. It must redirect to https://. Check for mixed content in the browser console — any http:// resource loaded on an HTTPS page is a warning.

Step 19. Check security headers via securityheaders.com.

Target: an A or A+ grade. The headers that actually matter are Content-Security-Policy (prevents XSS exfiltration), Strict-Transport-Security (locks in HTTPS), and Referrer-Policy (prevents data leakage via referer). The rest are defense-in-depth and should not block launch.

Step 20. Verify cookie flags.

In DevTools > Application > Cookies, check that every auth/session cookie has HttpOnly, Secure, and SameSite=Lax or Strict. A missing flag here is the difference between XSS being a bug and XSS being full account takeover.

Phase 6: Inputs and injection audit (15 minutes)

Step 21. Submit <script>alert(1)</script> in every text input.

Every form, comment box, profile field, search bar. Submit, then view the result page. If the alert fires, you have reflected XSS. If the text renders verbatim into the HTML source (but does not fire because of framework escaping), you are probably safe — but still check that the app does not use dangerouslySetInnerHTML anywhere with user-controlled content.

Step 22. Submit SQL injection payloads in every query input.

Submit values like ' OR 1=1 --, admin' --, " OR "1"="1. If the response differs from a normal submission (error page, different row count, stack trace), investigate further. Fix by using parameterized queries everywhere and never concatenating user input into SQL strings.

Step 23. Submit SSRF payloads on any field that takes a URL.

If the app accepts URLs from users (avatar upload from URL, webhook URL, import from URL), try values like http://localhost, http://169.254.169.254/latest/meta-data, file:///etc/passwd. Responses that return content for these are full-compromise findings — an attacker reads your cloud metadata endpoint and steals IAM credentials.

Phase 7: Final review

Step 24. Run a VibeArmor scan.

Regardless of how thorough the manual audit was, run the automated scan before launch. The manual audit catches 80% of what matters. The scanner catches the remaining 20%, which includes findings a human cannot easily check: cross-service attack chains, subdomain takeover, exposed GraphQL introspection, dependency vulnerabilities, and the long tail of framework-specific issues. Cross-check: every finding the scanner flags should either match something you already fixed, or be something you address now.

Step 25. Document the audit.

Write down the date, the URL audited, who ran it, every step's outcome, and every finding you fixed. This becomes your security posture history and the baseline for the next audit. If a customer asks whether you have done a security review, this document is the answer.

The reality of this checklist

Across the 17 apps in our production portfolio, the first time we ran this audit against each one we averaged 3.8 findings per app, with a range from 0 (a deliberately-hardened app) to 9 (a two-week-old Lovable MVP). The post-fix audit averaged 0.4 findings per app — most of the remaining issues were Tier 3 informational items that did not affect security posture.

The key insight is that vibe-coded apps do not have exotic vulnerabilities. They have five or six boring ones, over and over, and the same audit catches them every time. Run this checklist before every launch, and run it automatically every week afterwards. VibeArmor's $99 (one-time) Vibe Check tier is literally this checklist executed weekly with email alerts when anything regresses.

Frequently asked questions

How long should a security audit actually take?

A manual audit following this checklist takes 60-90 minutes for a developer familiar with the app. An automated scan via VibeArmor takes three minutes. For a major release, do both — the scanner catches issues the manual audit misses, the manual audit catches context-specific business logic flaws the scanner cannot know about.

Should I audit staging or production?

Both. Audit staging first so you find issues without putting real user data at risk. Audit production after launch to confirm the deployment did not introduce anything new. Production auditing does not require elevated access — everything in this checklist is external testing.

Do I need to do this for every deploy?

Not the full manual checklist. Run the automated scan every deploy — that is the entire point of the $99 (one-time) Vibe Check tier. Do the full manual walkthrough before each major release (monthly at most, quarterly is fine for mature apps). If the scanner flags a regression between audits, do a targeted manual check of the affected area.

What if I cannot fix everything before launch?

Prioritize by tier. Never ship with open Tier 1 findings (exposed secrets, broken auth, broken RLS, injection). Tier 2 findings (missing CSP, missing rate limiting) are acceptable to defer for a week if necessary, but not indefinitely. Tier 3 is informational and does not block launch. If a Tier 1 finding cannot be fixed quickly, take the affected feature offline until it can be.

Is this only for AI-coded apps?

The checklist works for any web app. It is tuned to AI-generated code because that is what fails these checks most often, but the vulnerabilities are universal. Human-written apps pass the audit more often, not because humans are infallible, but because a team with a code reviewer typically catches exposed secrets and broken RLS before deployment.

What is the single most valuable step?

Step 1, without question. Finding an exposed secret in the client bundle and rotating it before an attacker does prevents every downstream consequence of that key. If you only have ten minutes to spend on security this week, spend them on Steps 1-4.

Related reading

Scan your app free

Paste a URL, get a letter grade and Cursor-ready fixes in 3 minutes. No signup required.

Start Free Scan