[AI_SECURITY]

Supabase Security: What an External Scan Can (and Cannot) Tell You

Supabase security comes down to two things an external scanner cannot see — your Row Level Security (RLS) policies and whether your public anon key is properly gated by them — and one thing it can: a leaked secret. The honest summary is that an external scan can catch an exposed service-role key (the one that bypasses all your security), check your TLS, headers, and exposed files, but it cannot test your RLS policies. Confirming RLS is correct is something you must do yourself, inside Supabase.

This guide is deliberately straight about that boundary, because the boundary is the point. If a tool tells you it "scanned your Supabase security" and gives you a green checkmark on RLS from the outside, it is bluffing. Here is what's actually true, key by key.

Is the Supabase anon key safe to expose?

Yes — and this is the part that confuses people. The anon key (also called the publishable key) is designed to live in your frontend. It ships in your JavaScript bundle, it's visible in DevTools, and that is intentional. Supabase's own API keys documentation describes it as a public client key.

But "safe to expose" has a condition attached: the anon key is safe only if Row Level Security is enabled and correctly configured on every table it can reach. The anon key grants exactly the access your RLS policies allow — no more, no less. With good policies, a stranger holding your anon key can do only what an anonymous user is supposed to do. With missing or sloppy policies, that same public key can read or write data it never should.

So the key isn't the risk. The risk is whether the policies behind it are right. This is the single most important sentence in Supabase security: the anon key being public is fine; the question is what your RLS lets it do. This is the same publishable-vs-secret distinction we cover in how to find exposed API keys on your website — and Supabase is the textbook example of a key that is meant to be seen.

What is the service-role key, and why is it dangerous in the frontend?

The service-role key is the opposite of the anon key in every way that matters. It bypasses Row Level Security entirely. It has full read/write access to your whole database, ignoring every policy you wrote. It exists so your backend can perform privileged operations — and it must never, under any circumstances, reach the browser.

If the service-role key ends up in client-side code or a served file, RLS becomes irrelevant. It doesn't matter how carefully you wrote your policies; anyone who opens DevTools and finds that key can read, modify, or delete everything. This is the classic CWE-798: Use of Hard-coded Credentials, and with the service-role key the blast radius is your entire dataset.

This is exactly the kind of leak that creeps into AI-generated and "vibe-coded" apps, where a snippet wires the service-role key into a React component "to make it work" — see the security risks of vibe coding for why that pattern is so common. And it is the one Supabase secret that an external scan can catch for you.

Anon key vs service-role key, side by side

Anon (publishable) keyService-role key
PurposePublic client access from the browserPrivileged server-side operations
Safe in frontend?Yes, by designNo — never
What protects itYour RLS policies (the key alone grants nothing extra)Nothing — it bypasses RLS entirely
If it leaksNot a leak; it's meant to be publicFull database compromise — rotate immediately
Who verifies it's correctYou, by checking RLS inside SupabaseAn external scan can catch the leak; you rotate

What is Row Level Security, and how do I check mine?

Row Level Security (RLS) is the actual security boundary of a Supabase app. It's a PostgreSQL feature that attaches per-row access rules to a table, so a query only returns or modifies the rows the current user is allowed to touch. When you expose the anon key to the world, RLS is the only thing standing between an anonymous request and your data — the exact gap that played out at scale in the Lovable incident (CVE-2025-48757), where generated apps shipped the anon key with RLS left off.

The critical, non-negotiable fact: RLS correctness is something you verify yourself, inside Supabase. No external scanner can do it for you. Your policies live in your database catalog (pg_policies), not in any HTTP response — there is nothing on the wire for a black-box scan to read, so it has nothing to validate against.

Here is how to actually check it, yourself:

  1. Enable RLS on every public table. In the Supabase dashboard, any table reachable by the anon key must have RLS turned on. A table with RLS off is wide open to the anon key. Supabase's Row Level Security guide is the canonical reference.
  2. Write explicit policies. Enabling RLS with no policies denies everything; you then add SELECT/INSERT/UPDATE/DELETE policies that scope each operation (typically with auth.uid()). Test them as an anonymous user and as a logged-in user.
  3. Run Supabase's Security Advisor. The Supabase dashboard has a built-in advisor that flags tables with RLS disabled and other misconfigurations. Run it; fix what it flags.
  4. Query the catalog directly. You can confirm coverage in SQL by checking pg_tables.rowsecurity for every table in your public schema — anything false is unprotected.

That's the work an external scan cannot substitute for. Now here's what it genuinely adds.

What can an external scan tell me about my Supabase app?

An external black-box scan looks at your deployed site the way an attacker would — no repo access, no database access, no Supabase login. For a Supabase-backed app, here is what it can honestly verify, and what it cannot:

What protects a Supabase appWho can verify it
RLS policies are enabled and correctYou only, inside Supabase (dashboard advisor + SQL)
Anon key is properly gated by RLSYou only — depends on the policies above
Service-role key is not in client codeAn external scan can catch this leak
TLS, security headers, CORS, exposed filesAn external scan can check the deployed domain

So the genuine, no-overclaiming value of a scan like SteelSuit for a Supabase app is:

  • Catching a leaked service-role key. SteelSuit fetches your deployed HTML and JavaScript bundles and runs TruffleHog's 800+ detectors over them. It runs in pattern-match-only mode (--no-verification) — it never calls Supabase or any provider with a discovered key, because probing a third party with your credential isn't ours to do. So if a service-role key (or any other secret) is shipped in your client code, it gets flagged. Database-class detectors are flagged high. Treat every match as a real leak until you've rotated it.
  • Probing for exposed files. It checks paths like /.env, /.env.local, and /.git/config — common places a Supabase URL and service-role key get accidentally served.
  • Checking external posture. Valid TLS, security headers (CSP, HSTS, X-Frame-Options), and CORS behavior on whatever domain hosts your app — the same baseline from the web app security checklist.

A free fast scan takes about 26 seconds and needs no signup; the deeper scan (~5–6 minutes) runs after a one-time DNS-TXT proof you own the domain. Think of it as covering the half of Supabase security that's visible from the internet — while you handle RLS inside the dashboard.

What it does not do, and what no external scanner can do: it does not test your RLS policies. It cannot tell you whether your anon key is properly gated. Those checks live inside Supabase and only you can run them.

The takeaway

Securing a Supabase app is three layers, and they split cleanly by who can verify them. Inside Supabase, by you: enable and correctly configure RLS on every public table, and confirm it with the Security Advisor — this is the real security boundary, and no outside tool can vouch for it. Server-side, by you: keep the service-role key strictly out of the browser. From the outside, by a scan: catch a leaked service-role key, exposed .env/.git files, weak TLS, and missing headers.

The anon key being public is not the problem — that's by design. The problem is an unprotected table behind it, or a service-role key that escaped to the frontend. The first you must check yourself; the second an external scan will catch. Do both.

Frequently asked

Is it safe to expose the Supabase anon key?

Yes — the anon (publishable) key is designed to ship in your frontend, and it appears in client-side JavaScript by design. But it is safe ONLY IF Row Level Security (RLS) is enabled and correctly configured on every table it can reach. The anon key grants exactly the access your RLS policies allow; if those policies are missing or too permissive, the public key can read or write data it shouldn't. So the key itself is fine in the browser — the security boundary is your RLS configuration, which you must verify yourself inside Supabase.

What happens if my Supabase service-role key leaks?

The service-role key bypasses Row Level Security entirely and has full read/write access to your database. If it leaks into client-side code or a public file, anyone who finds it can read, modify, or delete all of your data regardless of any RLS policy. Treat it as a full database compromise: rotate the key immediately in your Supabase project settings, audit access logs, and move any code that used it to a backend so it never reaches the browser again. This is the one Supabase secret an external scanner can actually catch for you.

Can a scanner check my Supabase RLS policies?

No. An external black-box scanner cannot see or evaluate your Row Level Security policies — they live inside your database and are not exposed over the network. No outside scan, including SteelSuit, can confirm that your RLS is correct. You must verify RLS yourself inside Supabase: enable it on every public table, write explicit policies, and use Supabase's built-in Security Advisor and SQL queries to confirm no table is left unprotected. An external scan complements this by catching a leaked service-role key and weak external posture — it does not replace checking RLS.

How do I secure a Supabase app?

Three layers. First, configure Row Level Security: enable RLS on every table reachable by the anon key and write explicit policies — verify this yourself with Supabase's Security Advisor. Second, keep the service-role key strictly server-side; it must never appear in frontend code or any served file. Third, harden the deployed site: valid TLS, security headers, no exposed .env or .git, sane CORS. The first layer you confirm inside Supabase; the second and third an external scan can check from the outside.