No hacking — a passive scan only looks at what your browser already downloads when it opens a page. Here's what I found:
- 6 of 15 load their Supabase database directly client-side. The public API key sits in the page source. That's fine if Row-Level Security is configured right — but it's one wrong setting away from "anyone can read the whole table."
- 14 of 15 ship no Content-Security-Policy — a simple, high-value hardening against script injection, almost always missing.
Is this theoretical? No. Two apps I audited with the owner's permission:
- A social app: the profiles table — user names, cities, and a password hash — readable by a logged-out stranger. Closed in an afternoon.
- A paid learning app: 155 paid study sheets and 4,872 answers were readable by anyone, with no account and no subscription — its entire paid catalogue, a single API call away. The paywall lived only in the front-end; the database served everything to everyone.
Loading Supabase in the browser isn't the mistake. Not enforcing access in the database (RLS) is. And the tools you build with won't tell you — they'll happily ship it.
If you built something on Lovable / Bolt / Replit with real users (or paying ones), it's worth 60 seconds to check what a stranger can already see. I made a free tool that runs the surface check (passive, no signup): sealdy.dev
Happy to answer questions on how RLS leaks happen and how to lock them down.
Top comments (4)
This is a really important security finding. The fact that 40% of these apps expose database connections directly in the browser is alarming. Supabase does have Row Level Security which mitigates the blast radius, but I have seen too many projects where RLS policies are either missing or misconfigured. One thing I recommend to anyone using Supabase or similar backends is to treat your browser-exposed connection as untrusted by default and implement an API layer for anything sensitive. The convenience of direct access is not worth the risk when production data is involved.
Agreed on the principle — treat the browser-exposed client as untrusted, always. One nuance from the population I'm looking at, though: these apps are built by non-technical founders on Lovable/Bolt, and "add an API layer" isn't something they can realistically do — the tools generate direct client access by default and that's what ships. So for that audience, my hierarchy is: (1) RLS deny-by-default on every table, policies as the only read path — done right, Postgres-enforced RLS is a legitimate security boundary, not a mitigation; (2) test it by actually querying as the anon role, because the failure mode is almost never "no RLS", it's "RLS enabled, one policy too broad"; (3) an API/edge-function layer for anything that's business logic (payments, quotas, the paywall case from the post — that one can't be solved by RLS alone). The paid-catalogue leak I mentioned is exactly your point: entitlement checks lived only in the front-end.
I honestly expected it to be worse
Fair! Two honest caveats though. First, this was a passive scan — it only sees what a browser downloads, so it's a floor, not a ceiling. The 40% number means "database reachable from the browser", not "database leaking" — whether it leaks depends on RLS, which I can't test without the owner's permission. Second: the two apps where I did get permission to go deeper both turned out to be leaking (a profiles table with a password hash, and a full paid catalogue with no paywall on the API side). Small sample, but 2 for 2 — so "worse than it looks" is exactly what I'd expect for the rest.