Why this check matters for a Lovable app
In 2025 the National Vulnerability Database published CVE-2025-48757. Its description reads: an insufficient database Row-Level Security policy in Lovable through 2025-04-15 allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites. The record carries a note that the supplier disputes it, on the basis that each customer of the platform accepts responsibility for protecting the data of their own application. The entry is tagged disputed and its status is deferred.
Read both halves of that. The mechanism is real: a Supabase table without Row Level Security is open to anyone holding the public key, and that key ships in your app by design. The dispute is about whose job it is to close it. Whichever side you take, the practical answer is the same: it is your data, so it is your check to run. Nothing in this post requires you to believe Lovable is careless. It only requires you to look. One rule before you start: this self-check is for a project you own. Do not run the two-account test or any query against someone else's app.
What does Lovable's security scan check?
Lovable ships two scanners. The Basic scan runs in the background when you open the publish dialog. Per Lovable's docs it covers RLS policy linting, which checks row-level security policies for common mistakes, a database schema and access-control review, and a dependency audit for known vulnerabilities in npm packages. The Deep scan is a separate, on-demand review that does not run automatically as you work. It adds an access-control review for overly permissive data-access rules and database functions that bypass row-level security, checks that edge functions and APIs have authentication, and looks for exposed secrets, unsafe input handling such as SQL injection or XSS, insecure storage settings and information leakage.
Now the limits, in Lovable's own words: these tools help identify common security issues, but they cannot guarantee complete security. Publishing with unresolved critical issues is possible, but strongly discouraged, so the scan does not block you unless a workspace admin has enforced stricter rules. For apps handling sensitive data or critical functionality, the docs recommend an additional professional security review.
What that means in practice: the scanner reads your project from the inside. It can tell you a policy looks wrong. It cannot know what your app is supposed to allow. A policy that lets every signed-in user read every order is a valid policy to a linter and a data leak to your customers. That gap is what the rest of this check fills.
Which tables have RLS on, and does it limit anything?
Supabase exposes your Postgres tables through an auto-generated API. Its docs state that a table in an exposed schema without RLS is readable and writable by any role with a grant on it. The anon role, which is what the browser uses, usually has that grant. So step two is simply: which of my tables have RLS off? Open your Supabase project, go to the SQL editor, and paste the query below. Any row that says false is a table anyone can read with your public key.
Enabling RLS with no policies denies all API access to that table, which is the safe default. The Supabase docs show the pattern: enable row level security on the table, then add a policy that works like a WHERE clause on every query, such as allowing select to authenticated users only where auth.uid() equals the row's user_id. A policy whose condition is simply true does nothing to limit rows, so also look at the policies themselves in the Authentication section under Policies, and read each condition as if you were a stranger with a login.
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;The two-account test
This is the single most useful five minutes in the whole check, because it tests what your app actually does rather than what a policy says. A scanner reads rules; this reads results. You need two email addresses you control, the published URL of your app, and a private browser window so the second account does not inherit the first account's session.
If account B can see anything that belongs to account A, you have found a leak, and it exists no matter what the scanner said. Note which screen showed it and ask Lovable to add or fix the RLS policy on that table so that rows are limited to the owner. Then repeat the test. Do the same logged out: open the app in a private window and see what loads before you sign in.
- 1Sign up as account A on the published URL. Create some real-looking data: a record, a note, an upload, whatever your app stores.
- 2Sign out completely. Open a private browser window.
- 3Sign up as account B. Go through every screen, list and search box. Look for anything account A created.
- 4Try the URLs directly. If account A's item lived at a path with an id in it, paste that path while signed in as B.
- 5Try to change something. Editing or deleting account A's data as B is a write leak, which is often worse than a read leak.
Are your storage buckets public?
Uploads are where personal data tends to be: profile photos, documents, scans, receipts. Supabase Storage has public and private buckets. The docs are direct about the difference: files in a public bucket are already publicly accessible, so no policy is needed to read them. Private buckets require RLS policies on the storage.objects table before anyone can upload or download, and by default Storage does not allow any uploads to buckets without RLS policies.
Open Storage in your Supabase dashboard and look at each bucket. A public bucket is fine for a logo. It is not fine for anything a user uploaded about themselves, because the file URL can be shared or guessed and there is no login check. If a bucket that holds user files is public, ask Lovable to make it private and to add storage policies that limit reads and writes to the owning user, typically by matching the folder name to auth.uid(). Then rerun the two-account test on the upload screen.
Which Supabase key is in your browser?
Supabase has two kinds of keys, and the whole model depends on using the right one in the right place. The publishable key, and its legacy version called the anon key, is designed to be exposed: Supabase lists web pages, mobile apps, GitHub actions, CLIs and source code as safe places for it. It does not carry privileges on its own. It tells Postgres the request is from the anon or authenticated role, and RLS decides the rest. The docs also note that Supabase is deprecating the anon and service_role keys by the end of 2026 in favour of the sb_publishable and sb_secret formats.
The secret key, and its legacy version called service_role, is the opposite. Supabase's wording: a secret key bypasses every Row Level Security policy you have. If that key ever reaches the browser, none of your policies matter. Check by opening the published app, pressing F12, choosing Network, loading a page, clicking any request to your Supabase URL and reading the apikey header. A value that starts with sb_publishable is what you want. If you see sb_secret, or a long legacy token whose role field decodes to service_role, stop and rotate it from the Supabase dashboard, then ask Lovable to remove it from the frontend.
| Key | Legacy name | Safe in the browser? | Respects RLS? |
|---|---|---|---|
| Publishable key (sb_publishable_...) | anon | Yes, by design | Yes |
| Secret key (sb_secret_...) | service_role | Never | No, bypasses every policy |
Where do your API keys and secrets live?
Most Lovable apps call at least one outside service: an AI model, an email sender, a payment provider. Those keys must not be in the frontend, because the frontend is downloadable by anyone. The right home is a Supabase edge function, with the key stored as a secret that only the function can read. Lovable's docs say the platform detects API keys pasted into chat and guides you to store them securely rather than hardcoding them, so if you followed the prompts you are probably fine. Verify anyway.
Two checks. First, in the Supabase dashboard open Edge Functions and then Secrets, and confirm the keys you expect are listed there. Second, on the published app open the browser Network tab, filter for the third-party domain, and confirm the browser is calling your own edge function rather than the provider directly with a key in the request. If a provider key is visible in the browser, revoke it at the provider, create a new one, and add it as an edge function secret.
Test the published URL, not the preview
Lovable's Basic scan runs when you open the publish dialog, and that is also the right moment for your own checks. Everything above should be run against the published URL, because that is what your users get, and what the scanner reviewed is a snapshot of your project at that moment. If you keep building after publishing, the live app does not change until you publish again, so a fix you made in the editor is not protecting anyone yet.
Make it a habit: publish, run the two-account test on the live URL, and glance at the RLS query after any change that added a table. New tables are the ones most likely to have been created without a policy, because the feature worked in the preview while you were signed in as yourself.
What does an external scan add?
Lovable's scanner works from the inside, with your code and schema in front of it. An external scan works from the outside, the way an attacker does: it only has your published URL and whatever a browser can see. That is a different and complementary view. It can confirm that the public key really is the publishable one, that the tables your app touches refuse anonymous reads, that no secret appears in the shipped JavaScript, and that security headers are present, all without needing access to your project.
A read-only external scan such as VibeSecurity does not fix anything and does not replace the professional review Lovable's docs recommend for sensitive data. What it gives you is an independent answer to the question in the title, repeatable each time you publish, and evidence to show a customer who asks. If the inside scan and the outside scan both come back clean, and your two-account test passes, you have done more than most apps that launched this year.
- Basic scan and Deep scan run, criticals fixed.
- RLS query shows no public table with rowsecurity false.
- Two-account test passes on the published URL, signed in and signed out.
- User-upload buckets are private with owner-scoped policies.
- Only the publishable or anon key appears in browser requests.
- Provider API keys are in edge function secrets, not the frontend.
- External scan of the published URL is clean.
Frequently asked questions
Does Lovable's security scan make my app secure?
It helps, but it is not a guarantee. Lovable's docs say the scanners identify common security issues and cannot guarantee complete security, and they recommend a professional review for apps handling sensitive data. The scan can flag a suspicious policy; it cannot know what your app should allow. Run the two-account test yourself to confirm real users cannot see each other's data.
Is the Supabase anon key in my Lovable app a security problem?
No. The anon key, now called the publishable key, is designed to be public and Supabase lists source code and web pages as safe places for it. It carries no privileges on its own; Row Level Security decides what each request may read or write. The problem is a table without RLS, not the key. The service_role or secret key is the one that must never reach the browser.
How do I check whether RLS is enabled on my Supabase tables?
Open the SQL editor in your Supabase project and run: select tablename, rowsecurity from pg_tables where schemaname = 'public'. Any table showing false has RLS off and is readable and writable through the API by anyone holding your public key. Enable RLS on it and add a policy that limits rows to their owner, then retest with a second account.
What was CVE-2025-48757 about?
Per the National Vulnerability Database, CVE-2025-48757 describes an insufficient database Row-Level Security policy in Lovable through 2025-04-15 that allowed remote unauthenticated attackers to read or write arbitrary tables of generated sites. The supplier disputes it, saying each customer is responsible for protecting their application's data. The entry is marked disputed and deferred.
Should my Supabase storage bucket be public or private?
Use a public bucket only for files anyone may see, such as logos. Supabase documents that public bucket files are publicly accessible with no policy needed. Anything a user uploads about themselves belongs in a private bucket with storage policies that limit reads and writes to the owning user, usually by matching the folder path to the user's id.