The AI app security checklist
Whatever built it โ Lovable, Bolt, v0, Cursor, Claude Code โ the same handful of problems come up again and again. Here they are in the order worth fixing them.
These aren't obscure vulnerabilities. They're the consequences of two facts that are obvious once you know them and invisible until you do.
One. Browser code cannot keep a secret. Anything in the
frontend โ hardcoded, in a NEXT_PUBLIC_ variable, inside an SDK
running client-side โ is readable by every visitor. Not obscure. Public.
Two. Deleting a leaked key doesn't un-leak it. It's in your git history, someone's cache, a scraper's database. The only real fix is rotating it at the provider.
Critical โ fix before anyone else sees the app
Secret keys in code that ships to the browser
Stripe secret keys (sk_live_โฆ), AI provider keys, database
connection strings with passwords in them. Anything in a frontend file, or in
a variable prefixed VITE_ / NEXT_PUBLIC_ /
REACT_APP_, is published. Move the call server-side, read the key
from a server-only environment variable, then rotate the exposed one.
Database rules turned off
If you're on Supabase or Firebase, your client talks to the database
directly using a public key, and the only thing protecting your data is Row
Level Security (or Firestore rules). Off, or set to allow-all, means anyone
can read and write everything. A Supabase service_role key in
client code has the same effect, since it bypasses those rules entirely.
A live .env or .git on your deployed site
If yoursite.com/.env returns a file, every credential in it is
public and being scanned for constantly. Same for a served .git
directory, which lets anyone clone your whole history โ including secrets you
committed and later "removed".
High โ fix before you have real users
AI APIs called straight from the frontend
dangerouslyAllowBrowser: true is the usual marker. It makes
an error go away, the app starts working, and your API key is now in every
visitor's browser. Proxy the call through your own backend route and rate
limit it, or you're funding strangers' usage.
SQL built by pasting values into strings
A query assembled with an f-string or template literal will happily execute whatever a user types. Use parameterised queries โ placeholders with the values passed separately โ everywhere, not just where input "looks" untrusted.
No rate limiting on anything that costs money
Any endpoint that sends email, calls an AI model, or writes to your database can be called in a loop by a stranger. This is the one people skip because it isn't a "vulnerability" โ it's just a bill.
Medium โ fix before you forget
- CORS set to
*โ combined with cookie login, other sites can make requests as your users. - HTML built from user input โ
innerHTMLordangerouslySetInnerHTMLwith anything a user typed is how XSS gets in. UsetextContent, or sanitise. - Debug mode left on โ shows your code and variables to visitors on any error, and in Flask's case allows running code on your server.
- Published source maps โ let anyone rebuild your original source from the deployed bundle.
The five-minute version
- No secret keys anywhere in frontend code or public-prefixed variables
- Database rules on, and scoped to the logged-in user
yoursite.com/.envreturns nothing.envis in.gitignore- AI and payment calls go through your backend, with a rate limit
- Every key that was ever exposed has been rotated
Or have it checked for you
vibecheck scans your code or your live URL for everything on this page, explains what it finds in plain English, and gives you a fix prompt to paste back into whatever AI tool you're using.
Scan my app โ free