arrow_back Back to Blog
lovable securitysupabaserls

Lovable Security: What the Scan Won't Catch

·

Lovable security has gotten a lot better this year, and I want to say that up front because the rest of this post is going to sound grumpy. There’s a Quick security scan that runs every time you publish, a Deep security scan that reads your code, and a chat-based Security Review you can ask for. A year ago the most common thing we found in a Lovable app was a table with RLS switched off entirely. We almost never see that now.

What we see instead is an app with a green “passed” badge and a policy like this on the profiles table:

create policy "Users can view profiles"
on public.profiles for select
using (auth.uid() is not null);

Read it slowly. It says any logged-in user can read every row. Not their own row. Every row. Emails, phone numbers, the stripe_customer_id column someone added in week three, the lot. RLS is enabled, a policy exists, the policy even checks auth. A linter looking for “is there a policy and does it mention the user” is satisfied. The policy is still wrong, because the line should be auth.uid() = id.

That is the whole problem with automated checks in one example. They’re good at presence. Is RLS on, is there a secret in the bundle, is a dependency on a known-bad version. They’re weak at intent, and intent is where real Lovable apps leak.

How we actually test it

The fastest test needs no tools at all. Sign up to the app twice with two email addresses, open dev tools on account B, and copy the request the app makes to Supabase when it loads account B’s data. Now change the filter. If the app fetched profiles?id=eq.<B's id>, delete the filter and send it again. If you get back more than one row, you have a leak, whatever the scanner said.

Then repeat that for every table the frontend touches. On a typical Lovable SaaS that’s 8 to 15 tables, and it takes about an hour with curl and the anon key. The anon key is public by design, by the way. It ships in your JS bundle and that’s fine. Founders panic about it constantly and it’s the one thing in the bundle that isn’t a problem. The anon key plus bad RLS is the problem.

The four holes we keep finding

Policies that check login but not ownership. The example above. It shows up on profiles, messages, orders and anything called documents. Usually the AI wrote a correct policy first, then someone hit an error (“new row violates row-level security policy”) and asked Lovable to fix it, and the fix was to loosen the policy until the error went away.

Edge functions that trust the body. A function receives { "user_id": "...", "plan": "pro" } from the client and writes it with the service role key. The service role key bypasses RLS completely, so the function is now the only thing standing between a user and every row in the database, and it believes whatever user ID the browser sends. Any function that uses the service role key has to read the user from the JWT (supabase.auth.getUser() with the request’s Authorization header) and ignore anything the client claims about who it is. Also check supabase/config.toml for verify_jwt = false. Sometimes it’s right, for a Stripe webhook for example. Often it was flipped to make a CORS error go away.

Stripe webhooks without signature checks. Speaking of which. A webhook function that parses event.type === "checkout.session.completed" and upgrades the user, without calling stripe.webhooks.constructEventAsync with the signing secret, will upgrade anyone who can POST JSON to a public URL. We found one of these on a client’s app that had been live for four months with paying customers. Nobody had abused it, as far as their Stripe logs could tell us, and I still wouldn’t want to bet a company on “as far as we can tell.”

Public storage buckets. Avatars in a public bucket are fine. ID scans, invoices and contract PDFs in a public bucket, with predictable paths like uploads/<user_id>/passport.jpg, are not. The bucket being public was usually a choice made in five seconds to make an image preview work.

The client who passed

One founder came to us with a marketplace app, around 600 users, and a screenshot of a clean scan. They wanted help with performance, not security. While we were profiling, I noticed the seller dashboard pulled payout data with a query filtered on the client side. The request itself asked for payouts?select=*, and the React component filtered the array down to the current seller with .filter().

So every seller’s browser downloaded every seller’s payout history, bank last-four digits included, and then politely hid the other rows. The table had RLS. The policy was using (true) for authenticated users, which had been added, according to the chat history, to fix “payouts not showing.” Fixing the policy took ten minutes. Finding every other place in the code that depended on the loose policy took most of two days, because four screens broke the moment the policy was tightened.

That’s the part nobody budgets for. Tightening security in a Lovable app always breaks something, because the loose policies exist precisely to make features work.

What I’d do this week

Run the Deep scan, not just the Quick one, and actually read the findings instead of clicking “fix all.” Then do the two-account test above on your three most sensitive tables. Grep your edge functions for SUPABASE_SERVICE_ROLE_KEY and check every one of them reads the user from the token. Look at your storage buckets in the Supabase dashboard and ask, for each public one, whether you’d be fine with the whole bucket on a forum.

If your app is on Lovable Cloud, some of this is harder to reach, since you don’t get the full Supabase dashboard and you can’t easily run your own SQL against policies. That’s one of the reasons founders move their backend to their own Supabase project once real users arrive, and it’s what our Cloud to Supabase migration covers, policies included.

And if you’d rather someone else did the two-account test across the whole app, that’s our Lovable security audit: a written report, the fixes and a re-test. Here’s how we work, or just book a call and bring the scan report.