ThisCalculator

Everyday

Supabase Security: 8 Data Leak Checks in 10 Minutes

Updated · Standard formula, shown below

Supabase Security: 8 Data Leak Checks in 10 Minutes

Yes, you can run eight meaningful Supabase security checks in about ten minutes, and this article lists them with exact SQL queries and console steps. These checks focus on the most common data leak vectors: exposed API keys, permissive Row Level Security (RLS) policies, public storage buckets, and over-privileged roles. They won’t replace a full audit, but they will catch the mistakes that cause most public data breaches on Supabase projects.

Background: Why Supabase Leaks Happen

Supabase is a hosted Postgres database with an auto-generated REST API (PostgREST) and storage. The API is public by design; anyone with the anon key can query it. Your safety net is Row Level Security (RLS) — if RLS is disabled on a table, the API returns all rows to anyone with the anon key. The default Supabase dashboard actually creates tables with RLS enabled, but it’s easy to disable it during development and forget to re-enable it. Storage buckets are similar: a public bucket means any file inside is readable without authentication. The checks below target these exact weak points.

Check 1: Find Tables with RLS Disabled

Run this in the Supabase SQL Editor (Dashboard → SQL Editor):

SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
AND rowsecurity = false;

Any table in the result is exposed. If you see profiles, orders, or similar, that’s a leak. To fix: enable RLS with ALTER TABLE tablename ENABLE ROW LEVEL SECURITY; then create a policy. For a quick temporary fix, you can revoke access from the anon role, but the proper solution is a policy.

Check 2: List All RLS Policies

RLS can be enabled but still leak data if a policy is too broad. Run:

SELECT tablename, policyname, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;

Look for policies with USING (true) — those allow any user to read all rows. For example, a policy like USING (auth.uid() = user_id) is safe; USING (true) is not. Also check WITH CHECK on INSERT/UPDATE policies; a WITH CHECK (true) lets anyone write arbitrary rows.

Check 3: Inspect Storage Buckets

In the Dashboard, go to Storage → Buckets. For each bucket, click the gear icon and check if “Public bucketâ€� is enabled. A public bucket means any file inside is accessible via a URL without authentication. If you have a bucket named avatars and it’s public, that’s often fine. But if you have invoices or documents and it’s public, that’s a leak.

You can also query the storage schema:

SELECT id, name, public
FROM storage.buckets;

If you find a sensitive bucket that’s public, you can make it private in the bucket settings. Then you’ll need to use signed URLs (valid for a limited time) to serve files to authenticated users.

Check 4: Review API Key Exposure

The anon key is meant to be public in your frontend. The service_role key is not — it bypasses RLS entirely and gives full database access. If you accidentally commit the service_role key to a public GitHub repo, anyone can read and write your entire database.

To check for exposure: search your GitHub repositories for service_role or SUPABASE_SERVICE_ROLE_KEY. Also check your deployed frontend code (browser devtools → Network → look at request headers) — the service_role key should never appear there. If you find it, rotate the key immediately in Dashboard → Settings → API. Rotation invalidates the old key.

Check 5: Test RLS with the Anon Role

You can simulate what an anonymous user sees by running a query as the anon role. In the SQL Editor:

SET ROLE anon;
SELECT * FROM your_table LIMIT 10;
RESET ROLE;

If you get rows back from a table that should be private, RLS is not working. Do this for each critical table. Remember that the anon role is what the public API uses, so this test exactly replicates what an attacker sees.

Check 6: Look for Over-Permissive Grants

Supabase creates default privileges for anon and authenticated roles. But you might have accidentally granted more. Run:

SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
AND grantee IN ('anon', 'authenticated')
ORDER BY table_name;

Look for DELETE or TRUNCATE on tables that shouldn’t allow it. For example, if anon has DELETE on profiles, anyone can delete all profiles. Revoke with REVOKE DELETE ON public.profiles FROM anon;

Check 7: Verify Realtime is Not Leaking

Supabase Realtime broadcasts database changes to connected clients. If you enable Realtime on a table, any client with the anon key can subscribe to changes on that table — even if RLS is enabled, Realtime historically had RLS bypass issues. Check your Realtime settings: Dashboard → Database → Replication. Look at the “supabase_realtime� publication:

SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';

If you see tables that shouldn’t be broadcast, remove them from the publication. Only enable Realtime on tables that genuinely need live updates (like chat messages or notifications).

Check 8: Check for Exposed Secrets in Edge Functions

Edge Functions often use environment variables for secrets. If you hardcoded a database password or a third-party API key inside a function, it’s visible to anyone who can read the function code. In the Dashboard, go to Edge Functions, select a function, and click “Manageâ€�. Check the environment variables — they are stored encrypted, but if you used a secret in the code itself (e.g., const key = "sk_live_..."), it’s exposed. Also check your Git history for committed secrets.

Frequently Asked Questions

How do I enable RLS on an existing table?

Run ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; in the SQL Editor. Then create a policy, for example: CREATE POLICY "Users can read own profile" ON public.profiles FOR SELECT USING (auth.uid() = id);. Without a policy, RLS blocks all access (even for the table owner) — that’s safer than no RLS, but you must add policies for your app to work.

What is the difference between anon and authenticated roles?

The anon role is used when no user is logged in. The authenticated role is used when a user is logged in via Supabase Auth. Both roles have their own grants and RLS policies. Typically you allow anon to read public data (like a product list) and authenticated to read/write their own data (like a profile).

Can I use the service_role key on the client?

No. The service_role key bypasses RLS and has full admin privileges. If it’s in your frontend, anyone can extract it and take over your database. Only use it in server-side code (Edge Functions, your own backend) or in trusted admin tools.

Why does my table show rows when I query with the anon role, even though RLS is enabled?

RLS policies might be too permissive. Check the USING clause — if it’s USING (true), it allows all reads. Also, if you have a policy that uses auth.uid() but the anon role is not authenticated, auth.uid() returns null, so the policy may fail closed (no rows) or fail open depending on how you wrote it. Always test with SET ROLE anon to see exactly what an anonymous user sees.

How often should I run these checks?

Run them after any schema change, before a production release, and periodically (e.g., monthly). Also run them immediately if you suspect a breach. Automating the SQL queries in a CI pipeline can catch regressions early.

What if I find a leak? What’s the first step?

Immediately revoke access: disable RLS? No — enable RLS on the exposed table, or revoke the anon role’s privileges. Then rotate any exposed keys. Then assess what data was exposed (check logs for unusual API requests). Finally, fix the root cause — usually a missing policy or a public bucket — and re-run the checks.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.