In short
- Test the live app, not the code: a generated app can look right in the editor while the deployed database answers strangers.
- Data comes first: RLS or Firebase rules on every table, with an owner check, and private buckets for private files. In the scan behind CVE-2025-48757, 170 of 1,645 Lovable apps had missing or weak RLS.
- A
VITE_orNEXT_PUBLIC_variable your code reads ships to every visitor. Secret keys belong on the server, and a leaked one is revoked, not just deleted from the code. - Per-user access needs two accounts you own: create a record as A, request it as B. A single-account test cannot find this bug.
- Rerun the checklist after every deploy that touches the database, auth, routes or dependencies. A new table or route is where old bugs come back.
On this page
- How to use this checklist
- 1. Data access: rules on every table and every bucket
- 2. Keys: no secret in the frontend
- 3. Auth: email confirmation, rate limits and the reset flow
- 4. Per-user access: user B cannot read user A's rows
- 5. AI endpoints: a session, a quota and a budget
- 6. Files and headers
- 7. Framework versions and known CVEs
- 8. Deploys: retest after every change
- The traps that survive a quick scan
- Questions
- Sources
How to use this checklist
This checklist follows the order in which a stranger looks at an app built with Lovable, Bolt, Replit, v0, Cursor or Claude Code: the data first, then the keys, then the accounts. Each item comes with a test you can run on your own live app in a few minutes. Tick it when you have seen the result, not when your coding agent says it is fixed: ask the agent for the evidence, such as the empty query result or the 401.
The live app is what counts. In March 2025, a researcher scanned 1,645 apps built with Lovable: 170 of them, about one in ten, had missing or weak row level security that let unauthenticated visitors read or write data. That scan led to CVE-2025-48757, which Lovable disputes on the grounds that each customer is responsible for their own app's data. For why these bugs keep showing up in generated code, read vibe coding security risks.
Platform scanners help, so run them: Lovable's Quick scan checks database access rules, dependencies and MCP servers exposed without authentication on every publish, and Supabase's Security Advisor lists issues in your database. Both inspect your project from the inside. Neither signs in as two different users and tries each other's records, which is where the worst bugs hide.
| Layer | What goes wrong in generated apps | Live test |
|---|---|---|
| 1. Data | A table with RLS off, a policy that lets everyone in, a public bucket | Read a table with the public key and no session |
| 2. Keys | A secret key shipped in the JavaScript bundle | Search the live bundle for secret key prefixes |
| 3. Auth | Instant accounts, no throttling, reset links that last for days | Sign up with a throwaway address, try wrong passwords on your own account |
| 4. Per-user access | User B reads or edits user A's records by changing an id | Two accounts you own, one record, swap the session |
| 5. AI endpoints | A route that calls a paid model for anyone | Call it from a terminal with no cookie |
| 6. Files and headers | .env or source maps served, browser headers missing | Request the files, read the response headers |
| 7. Framework | A Next.js or React version with a known CVE | Read the exact version in the lockfile |
| 8. Deploys | A new table or route ships without the fix | Rerun the list after each deploy |
1. Data access: rules on every table and every bucket
Your Supabase publishable key, or the older anon key, ships in your frontend, and so does your Firebase config. That is by design. What protects the data is row level security (RLS) in Supabase and Security Rules in Firebase: rules inside the database that decide, row by row, who can read and write. Supabase's own production checklist says it plainly: a table without RLS and reasonable policies lets any client read and change its data.
- RLS is on for every table in every schema your API exposes, including tables your agent created in a SQL migration. The query below returns no rows.
- No policy on a table with personal data uses
trueas its condition, and no policy forauthenticatedstops at checking that someone is signed in without comparing a column toauth.uid(). - Tables in the Realtime publication have RLS and policies too. Supabase notes that RLS does not filter Postgres Changes
DELETEevents, so send sensitive changes through Broadcast with Realtime Authorization. - Firebase: no rule says
allow read, write: if true, and no rule stops atrequest.auth != null. Firebase's documentation warns that the second one gives any logged-in user access to the data. - Storage buckets that hold invoices, ID scans or private uploads are private, with a policy that limits each user to their own folder.
- Security Advisor in the Supabase dashboard shows no errors. Treat each warning as a lead and confirm it with the tests here.
-- Tables in public with RLS off. The answer should be empty.
select schemaname, tablename
from pg_tables
where schemaname = 'public'
and not rowsecurity
order by tablename;Then test from outside, the way a visitor would. Pick a table your app reads, such as orders or profiles, and ask for it with nothing but the public key:
curl "https://YOUR_PROJECT_REF.supabase.co/rest/v1/orders?select=*&limit=1" \
-H "apikey: YOUR_PUBLISHABLE_KEY"[] on a table you know has data means RLS filtered it out.The fixes: policies for the usual cases in the Supabase RLS guide, the per-folder bucket policy in the storage guide, and owner-only rules in the Firebase rules guide.
2. Keys: no secret in the frontend
Anything your build puts in the JavaScript bundle is public. Vite bundles the VITE_ variables your code reads into the JavaScript it sends to the browser, and Next.js inlines NEXT_PUBLIC_ variables the same way. That is fine for a Supabase publishable key, a Stripe pk_ key or a Firebase web config, whose safety comes from the rules behind them. It is a leak for any key that can spend money or skip your rules.
- On your live site, open the developer tools and search all loaded files (Ctrl+Shift+F in Chrome, Cmd+Option+F on a Mac) for
sk_live_,sk-proj-,sk-ant-,sb_secret_andPRIVATE KEY. Nothing matches. - A legacy Supabase service role key is a JWT starting with
eyJ, and its role is encoded inside it, so a text search forservice_rolemisses it. Paste eacheyJtoken you find into the Supabase key checker: only the anon key belongs in the browser. - No server secret in your
.envhas aVITE_orNEXT_PUBLIC_prefix. Watch for a fix that adds the prefix to make an "undefined" error go away: it moves the secret into the bundle. - Calls to OpenAI, Anthropic, Stripe or your email provider go through your server or an Edge Function that holds the key.
- A secret that was ever in a build, a commit or a screenshot has been revoked at the provider and replaced. Deleting the line is not enough: old builds, caches and git history still hold the value.
- After a rotation, a test call with the old key fails.
The API key leak checker names each key in code you paste and says which ones are secret. If one is, follow what to do with a leaked API key: revoke first, then read the provider's usage logs. The longer list of keys that belong only on a server is in API keys exposed in the frontend.
3. Auth: email confirmation, rate limits and the reset flow
Generated sign-up flows work on the happy path. These checks cover what a stranger tries next. The values are Supabase's documented defaults; other auth providers have the same settings under other names.
- Email confirmation is on, under Authentication > Sign In / Providers in Supabase. Without it, a stranger gets a session in one request, and every policy that only checks for a signed-in user lets them in.
- Sign-in is throttled. By default, Supabase Auth allows 30 requests per 5 minutes per IP address on sign-up, magic link, OTP and password recovery, and 150 per 5 minutes on the token endpoint that handles password sign-ins and refreshes, then answers
429. If you wrote your own login route, try a dozen wrong passwords on your own test account and expect a429or a CAPTCHA. - Reset and confirmation links expire within an hour. Supabase's checklist asks for an OTP expiry of 3600 seconds or less, and that one setting also controls confirmation, recovery, email change and invitation links.
- The reset endpoint's answer never contains the reset token, and the link in the email points to your domain even when the request carries another
HostorX-Forwarded-Hostheader. - Site URL under Authentication > URL Configuration is your production domain, not
http://localhost:3000, and the Redirect URLs list holds only your own domains. - Sign-up, sign-in and reset answer the same way for an email that has an account and one that does not, so nobody can test which addresses are registered.
- Test phone numbers and test accounts with known passwords are gone before launch.
- Auth emails go through your own SMTP. Supabase's built-in provider sends 2 auth emails per hour for the whole project, so confirmations and resets stop arriving on launch day.
4. Per-user access: user B cannot read user A's rows
This is the bug a single-account test never finds, and one we see often in generated apps. The app checks that you are signed in, then returns whatever record the id in the request names. It is called an IDOR, an insecure direct object reference, and it lives in API routes and in database policies alike.
- 1
Create two accounts you own
Use two browsers, or a normal window and a private one, so each keeps its own session. Call them A and B.
- 2
Create a record as A
An order, a note, a file, a message. Copy its id from the address bar or from the request in the Network tab of the developer tools.
- 3
Ask for it as B
Open the same address in B's window. Then replay A's API request with B's session: right-click it in the Network tab, choose Copy > Copy as cURL, and swap the cookie or
Authorizationheader for B's. - 4
Try to change it
As B, edit and delete A's record through the same requests. Then repeat everything signed out.
- 5
Read the answer
A
403, a404or an empty list is a pass. A's data, or a change that sticks, is a leak.
Add one more test: send an update for your own profile with an extra field such as role, plan or is_admin. If it sticks, any user can promote themselves. The fix for both bugs belongs on the server or in the database, on every read and every write: the record must belong to the person asking, and the server decides which fields a user may change.
create policy "Owners read their orders"
on public.orders for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Owners update their orders"
on public.orders for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);using clause filters what a user can see and change, with check stops them from moving a row to someone else.// Before: returns any order to any signed-in user
// const order = await db.order.findUnique({ where: { id } });
// After: the order must belong to the signed-in user
const order = await db.order.findFirst({
where: { id, userId: session.user.id },
});
if (!order) return new Response("Not found", { status: 404 });5. AI endpoints: a session, a quota and a budget
Every route that calls a model costs money on each request. OWASP's Top 10 for LLM applications calls the risk Unbounded Consumption, and names one form of it Denial of Wallet: an attacker sends enough requests to run up the app owner's bill. A chat route that answers without a session is a free model API for whoever finds its address in your JavaScript.
- Every route and Edge Function that calls a model requires a signed-in session, checked on the server. The test below returns
401or403. - Each user has a quota per minute and per day, counted on the server, not in the browser.
- The server caps input length and output tokens and picks the model. The client sends a message, not a model name or a token limit.
- A spending cap or usage alert is set in your model provider's dashboard, if it offers one, low enough that you hear about abuse before the invoice.
- The system prompt holds no keys, and no tool the model can call skips the access checks the rest of your API applies.
# Your own app only. No cookie, no token: this must fail.
curl -i -X POST https://your-app.example/api/chat \
-H "Content-Type: application/json" \
-d '{"message":"Say hello"}'The unprotected AI endpoint guide shows how to add the session check and a per-user quota without breaking streaming.
6. Files and headers
Your host serves what ends up in the build output, and some setups serve more than you meant. These checks take a few minutes with a terminal. For the headers, the security headers guide has the config for each host.
/.env,/.env.localand/.git/configreturn404on your live domain, or your home page, never their contents.- Source maps (
.js.mapfiles) are not served in production, unless you mean to publish your original source code. - Your server sends
Strict-Transport-Security, aContent-Security-Policythat includesframe-ancestors,X-Content-Type-Options: nosniffand aReferrer-Policy. - Those headers come from your server or your host's config, not from a meta tag in the HTML: browsers ignore
X-Frame-Optionsandframe-ancestorsin a meta tag, so the protection against framing silently disappears. - Session cookies carry
Secure,HttpOnlyandSameSite. - Your API never echoes the caller's
OrigininAccess-Control-Allow-Originwhile also sendingAccess-Control-Allow-Credentials: true.
# Your own domain only. Each file should answer 404, or your home page.
for p in /.env /.env.local /.git/config; do
printf '%s ' "$p"; curl -s -o /dev/null -w '%{http_code}\n' "https://your-app.example$p"
done
# The security headers on your home page
curl -sI https://your-app.example | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy'200 is not always a leak: single-page apps often answer every path with the home page. Open the address and look at what comes back.If a .env file does come back, treat every key in it as leaked. The exposed .env file guide covers the cleanup.
7. Framework versions and known CVEs
Your agent installs the version it knows, and nothing upgrades it after launch unless you do. A framework flaw hits every app on the affected versions, however careful your own code is.
- You know the exact versions running:
npm ls next react react-dom, or the lockfile, not the ranges inpackage.json. - Next.js is on the latest patch of a supported line. The Next.js November 2026 security release, published on 3 November 2026, shipped 16.4.5 (Active LTS) and 15.5.29 (Maintenance LTS).
- You are not on a version hit by React2Shell (CVE-2025-55182), remote code execution in React Server Components rated CVSS 10.0. It affects the React server packages 19.0.0, 19.1.0, 19.1.1 and 19.2.0, and Next.js 15 and 16 App Router apps before their fixed versions. The first React fixes (19.0.1, 19.1.2, 19.2.1) were followed by more flaws in the same code, such as CVE-2025-55184, so take the current patch, not the first one.
- Auth does not live only in middleware. CVE-2025-29927 let requests skip middleware in self-hosted Next.js before 15.2.3, 14.2.25, 13.5.9 and 12.3.5, so check the session again in each page, route handler and server action.
npm audit --audit-level=critical, or your host's dependency alerts, runs on every pull request. The command exits with an error when it finds a critical vulnerability, so it can block the merge.
Paste your package.json into the Next.js CVE checker to see each known CVE that applies to your versions and the version that fixes it. A version tells you what might apply. Keybrum proves a CVE by its behaviour on the live app instead, and never runs remote code execution or denial-of-service payloads to do it. More on the framework side in the Next.js security guide.
8. Deploys: retest after every change
A checklist is a snapshot. The table your agent adds next week, the new API route, the dependency bump: each can reopen a hole you closed. Supabase's own security checklist says to run it again whenever you add tables, sign-in methods or teammates to a live project.
- Every migration that creates a table enables RLS on it in the same file.
- The query from section 1 and Security Advisor run again after each schema change.
- On Lovable, the Quick scan runs on every publish. The Deep scan, which reviews your app's logic and permissions, usually does not run on its own: start it from the Security view before you publish.
- The two-account test from section 4 runs again after any change to routes, policies or auth.
- Preview deployments do not point at your production database.
- You keep a dated log of what was tested and what failed. It is also the start of your answer to the first security questionnaire a customer sends.
This is the part Keybrum automates. It attacks your live app the way a stranger would: it reads your tables with the public key, signed out and then as a brand-new account; with your consent, creates a test record with one account and requests it with another; calls AI routes and Edge Functions without a session; sends eight wrong sign-ins to a probe address to look for throttling; and checks for exposed .env files, source maps and missing headers. Each finding comes with the request that worked, and your coding agent reads the fix through MCP. Plans attack again on every deploy.
The traps that survive a quick scan
- The fix lives in the frontend. Hiding a button or a page does not stop the API call behind it. Every check in sections 1, 4 and 5 has to hold when the request comes from a terminal.
- Views and `security definer` functions. Views run with their owner's rights by default, and
security definerfunctions always do, so either can hand a locked table to the API. The RLS guide shows how to close them. - Unsigned payment webhooks. A webhook route that grants a plan without checking the
Stripe-Signatureheader against yourwhsec_secret and the raw request body gives a paid plan to anyone who posts to it. The Stripe webhook guide has the code. - Raw SQL and raw HTML. A query built by pasting user input into a string, or user content rendered with
dangerouslySetInnerHTML, brings back SQL injection and cross-site scripting. Pass values as query parameters and let React escape text.
Questions
What should a security checklist before launch include?
For an app built with AI: access rules on every table and bucket, no secret key in the frontend, email confirmation and rate limits on auth, a two-account test, a session and a quota on AI routes, the right files and headers, patched framework versions, and a retest after each deploy. Check each item on the live app.
Is there an official Lovable security checklist?
Lovable's security documentation centers on two built-in scans: a Quick scan that runs every time you publish, covering database access rules, dependencies and MCP server exposure, and a Deep scan that reviews your code's logic and permissions, which you usually start yourself. Run both. They read your project, while the signed-out and two-account tests on this page show what the live app actually returns. More in the Lovable security guide.
What is on the Supabase security checklist?
Supabase's production security checklist covers RLS on every table, including replicated ones, SSL enforcement, network restrictions, MFA on your Supabase account, email confirmations, an OTP expiry of one hour or less, and custom SMTP. It ends with a review of the risks specific to your app, which is where the per-user and AI route checks above come in.
How is an AI app security checklist different from a normal one?
It adds the routes that call a model. Each request costs money, so an AI route without a session or a quota can be drained by anyone, which OWASP calls Denial of Wallet. It also adds the model's tools: anything the model can call must apply the same access checks as the rest of your API.
Can my coding agent run this checklist for me?
Partly. It can run the SQL, read the policies and search the code for keys, and Supabase publishes a read-only audit prompt for agents in its production checklist. What it cannot see in the code is how the deployed app answers a stranger and a second account. Ask it for evidence on each item, then test the live app.
How often should I rerun a vibe coding security checklist?
After every deploy that touches the database, auth, API routes or dependencies, and before each launch or announcement. Keybrum plans attack the app again on every deploy and keep retests open.
Sources
- 1.Production security checklist, Supabase Docs
- 2.Rate limits, Supabase Docs
- 3.Production Checklist, Supabase Docs
- 4.Redirect URLs, Supabase Docs
- 5.Avoid insecure rules, Firebase Documentation
- 6.Security (Quick scan and Deep scan), Lovable Docs
- 7.Env Variables and Modes, Vite
- 8.Environment Variables, Next.js Docs
- 9.X-Frame-Options, MDN Web Docs
- 10.CSP: frame-ancestors, MDN Web Docs
- 11.LLM10:2025 Unbounded Consumption, OWASP Gen AI Security Project
- 12.Receive Stripe events in your webhook endpoint, Stripe Docs
- 13.November 2026 Security Release, Next.js
- 14.React Server Components are Vulnerable to RCE (GHSA-fv66-9v8q-g76r), GitHub Advisory Database
- 15.Authorization Bypass in Next.js Middleware (GHSA-f82v-jwr5-mffw), GitHub Advisory Database
- 16.CVE-2025-48757, NIST National Vulnerability Database
- 17.Statement on CVE-2025-48757, Matt Palmer