SUPABASE RLS CHECKER

Missing RLS is the #1 vulnerability in Lovable and Bolt apps. This tool tests every public table through the anon key — exactly like an attacker would.

TEST YOUR SUPABASE TABLES NOW

Enter your deployed app URL — we discover the Supabase project from your bundle and probe every public table with the anon key.

Why This Is the #1 Lovable/Bolt Vulnerability

Lovable and Bolt both default to Supabase. Supabase tables are created with RLS disabled by default. The AI scaffolds your app, data flows, everything works in preview — and in production, every row in users, orders, messages, documents is readable by anyone with a browser console.

We see this in roughly 85% of scanned Lovable apps.

The Supabase anon key is designed to ship to the browser — that’s how the client SDK reaches the database. The defence layer is RLS. When RLS is off, the anon key becomes a full read/write key for your entire schema, distributed to every visitor of your site.

What the Checker Does

  1. Discovers the Supabase project URL and anon key from your frontend bundle
  2. Enumerates public tables via the PostgREST ?select=* introspection on the OpenAPI spec at /rest/v1/
  3. Probes each table with an anon read, checking for:
    • Unrestricted read (critical)
    • Unrestricted write/delete (critical)
    • Missing user-scoped policy (high)
    • Over-permissive policy (medium)
    • Storage bucket access via storage/v1/object/public
  4. Reports each finding with exact SQL to fix and a verification query

Common Finding Types

TABLE FULLY PUBLIC

Anon key reads every row. Critical. Most common finding.

WRITE PERMITTED

Anyone can insert/update/delete. Catastrophic if not caught.

USER-SCOPE MISSING

RLS enabled but policy doesn't filter by auth.uid().

OVER-SHARED COLUMNS

RLS works but selects return email/phone/PII to every requester.

RPC WIDE OPEN

Postgres functions exposed via /rest/v1/rpc/* with no EXECUTE restriction.

STORAGE BUCKET PUBLIC

Public bucket with sensitive uploads (invoices, IDs, avatars containing EXIF).

VIEWS BYPASS RLS

Views defined as SECURITY DEFINER run as the owner — RLS on the underlying tables is silently bypassed.

SERVICE-ROLE KEY LEAK

Cross-check: if the bundle contains the service_role key (not just the anon key), every other RLS finding is moot.

How attackers find your Supabase project

The recon is one curl away:

  1. View source on your site, search supabase.co — the URL https://<projectRef>.supabase.co is right there along with the anon JWT.
  2. Decode the JWT at jwt.io. The ref claim confirms the project, role: anon confirms it’s the public key.
  3. Hit https://<projectRef>.supabase.co/rest/v1/?apikey=<anonKey> to get the OpenAPI spec listing every table, view, and function in your public schema.
  4. For each table, GET /rest/v1/<table>?select=*&limit=1000 with the anon key. If RLS is off, you get rows. Pagination via Range headers will return up to 10,000 rows per request.
  5. For writes, POST /rest/v1/<table> with a JSON body. If RLS is off (or only SELECT policies exist), inserts succeed.
  6. Storage: GET /storage/v1/bucket lists buckets. Each public bucket exposes /storage/v1/object/public/<bucket>/<path>.

The whole chain runs in seconds with no rate limit on a misconfigured project. Most exfiltrated Supabase databases are dumped this way — not via SQL injection, just via the published API.

Policy Starter Pack

Basic secure patterns (paste into Supabase SQL editor):

-- Enable RLS on a table (RLS is OFF by default — you must opt in)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Force RLS even for table owners — protects against future role drift
ALTER TABLE posts FORCE ROW LEVEL SECURITY;

-- SELECT: users read their own rows
CREATE POLICY "posts_select_own" ON posts
  FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

-- INSERT: users can only create rows they own
CREATE POLICY "posts_insert_own" ON posts
  FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = user_id);

-- UPDATE: users can only modify rows they own (and can't change ownership)
CREATE POLICY "posts_update_own" ON posts
  FOR UPDATE
  TO authenticated
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- DELETE: explicit, separate from update
CREATE POLICY "posts_delete_own" ON posts
  FOR DELETE
  TO authenticated
  USING (auth.uid() = user_id);

Deploy, rescan, repeat until the report shows zero criticals.

Common policy pitfalls

Mistake What it looks like Fix
RLS enabled, no policies ENABLE ROW LEVEL SECURITY without any CREATE POLICY — table is fully blocked, app breaks, dev “fixes” by disabling RLS again Add at least one SELECT policy with auth.uid() filter
USING (true) Policy exists but condition is always true Replace true with the user-scope filter
FOR ALL over-grants Single policy covers SELECT/INSERT/UPDATE/DELETE — easy to mis-scope Split into per-action policies
WITH CHECK missing on UPDATE User can update their row to set user_id to someone else’s Always pair USING with a matching WITH CHECK on UPDATE
Anonymous (anon) policies TO anon accidentally added — defeats the purpose Use TO authenticated unless you genuinely want public reads
Service role used in client Frontend uses service_role key “just to ship” — bypasses RLS entirely Rotate immediately, switch client to anon key
SECURITY DEFINER views View runs as the owner, ignoring caller’s RLS context Use SECURITY INVOKER (Postgres 15+) or rewrite as a function with row-filter logic

Verify a fix from the command line

After deploying a policy change, replay what the scanner does:

# Should return [] for a properly RLS'd table when called with anon key
curl "https://<ref>.supabase.co/rest/v1/posts?select=*&limit=1" \
  -H "apikey: <anon_key>" \
  -H "Authorization: Bearer <anon_key>"

# Should return your row(s) only when called with a real user JWT
curl "https://<ref>.supabase.co/rest/v1/posts?select=*&limit=1" \
  -H "apikey: <anon_key>" \
  -H "Authorization: Bearer <user_jwt>"

If the first call still returns rows, the policy isn’t filtering. The most common cause: a leftover USING (true) from before, or RLS was never enabled on this specific table (check pg_class.relrowsecurity).

What this scanner does NOT flag

  • Tables in non-public schemas. PostgREST exposes only the schemas you configure. If you’ve moved sensitive data to a non-exposed schema, the scanner can’t see it (which is the desired outcome, but worth noting).
  • Custom JWT claims used by your policies. If a policy uses auth.jwt()->>'org_id' and the scanner doesn’t have an org-scoped JWT, it can’t determine whether cross-org isolation works. We mark these Pass-with-caveat and recommend authenticated re-testing.
  • Realtime subscription channels. Realtime has its own RLS evaluation — the scanner tests REST, not WebSocket subscriptions. If your channel filters are wrong, an attacker can subscribe to other users’ inserts. Test channels manually with the JS client.
  • Edge Function authorization. Edge Functions run with the service role by default and bypass RLS unless you explicitly thread through the user’s JWT. Out of scope for this checker — covered by the full Vibe Code Scanner.
  • Database backup files. If pg_dump snapshots are accessible via Storage, no amount of RLS helps.

COMMON QUESTIONS

01
What does 'missing RLS' actually mean?
Supabase exposes your Postgres database over a public REST API. Without Row Level Security (RLS) policies, anyone with your anon key can read, modify, or delete any row in any public table. The anon key is in your frontend — so anyone can use it.
Q&A
02
How do I enable RLS?
In the Supabase dashboard, navigate to the table, enable RLS, then add policies like `auth.uid() = user_id` for reads. Or use SQL: `ALTER TABLE posts ENABLE ROW LEVEL SECURITY;` then `CREATE POLICY ...`. Rescan to verify.
Q&A
03
Does the tool store my data or scan results?
Scan inputs (URL) are stored for support. Data read from your tables during probing is not stored — only counts and table names (which are already public).
Q&A

AUDIT EVERY TABLE + AUTH FLOW

The VibeEval agent extends this into auth bypass testing and cross-user data probing.

RUN FULL SCAN