Fix Supabase RLS policy USING (true) that makes a table publicly readable
Updated · Applies to: Supabase, Lovable, Bolt, v0, Cursor, Replit, Claude Code
TL;DR fix: A policy like USING (true) or WITH CHECK (true) for anon or authenticated means every row is allowed for that operation. With a grant on the table, anyone who has your public anon (publishable) key can read or write the whole table through the Data API. Keep RLS on, drop the always-true policy, and replace it with a scoped check such as (select auth.uid()) = user_id. USING (true) on SELECT is only OK for data that is meant to be public.
Steps
- Confirm the table is reachable with the anon key. Your anon (publishable) key is meant to be public. The risk is the table behind it. In the browser Network tab or with curl, a GET to /rest/v1/<table>?select=* with only the anon key returning rows for a private table means the grant plus policy combo is too open.
- Open Security Advisor and list policies. In the Supabase dashboard go to Advisors → Security Advisor. Fix flags for RLS disabled and permissive policies (always-true). Also open Authentication → Policies (or the table in Table Editor) and read every USING / WITH CHECK expression.
- Find USING (true) and WITH CHECK (true). Any policy whose USING or WITH CHECK is literally true (or a tautology like 1=1) allows every row for that operation and role. On INSERT, UPDATE or DELETE for anon, that is almost never intentional. On SELECT it is only OK for genuine public catalogs.
- Drop the always-true policy and write a scoped one. Keep RLS enabled. Drop the bad policy, then create one that checks (select auth.uid()) = user_id (or your ownership column) for authenticated. Grant anon SELECT only when the data is meant to be public, and only with an intentional public-read policy.
- Retest as anon and as a signed-in user. Logged out: private tables should return empty or 401/403, not other users' rows. Logged in: you should see only your own rows. The SQL editor bypasses RLS, so always retest through the app or REST with the anon key.
- Re-run Security Advisor. Confirm the permissive-policy and RLS-disabled warnings are gone, then load the live app once more as a logged-out visitor.
The fix in SQL
Run this in the Supabase SQL editor. Replace profiles and user_id with your table and owner column. Keep RLS on.
-- Keep RLS enabled (do not disable it)
alter table public.profiles enable row level security;
-- Remove always-true policies (adjust names to match yours)
drop policy if exists "Allow public read access" on public.profiles;
drop policy if exists "Enable read access for all users" on public.profiles;
drop policy if exists "Allow all" on public.profiles;
-- Signed-in users read only their own rows
create policy "users read own rows"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);
-- Signed-in users update only their own rows
create policy "users update own rows"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
-- Optional: if this table must be readable when logged out (public catalog only)
-- grant select on public.profiles to anon;
-- create policy "public read catalog"
-- on public.profiles for select
-- to anon, authenticated
-- using (true); -- intentional public SELECT only — never for insert/update/delete
Supabase’s own RLS guide states that a policy to anon using ( true ) grants every unauthenticated visitor read access to every row the role can already reach through grants, and should be used only for data that is meant to be public. Security Advisor lint 0024_permissive_rls_policy flags always-true USING / WITH CHECK expressions for the same reason.
Why AI-built apps hit this so often
Coding agents often enable RLS (or inherit it from the Table Editor), then add a placeholder policy so the app “works” on the first run: using (true) or with check (true). That clears insert errors and blank lists, and looks like a successful fix in the chat transcript. It also recreates the failure mode behind CVE-2025-48757 and the open tables UpGuard counted at 16,326 on October 14, 2026: the public anon key can reach every row.
Related footguns we already document separately:
- Inserts blocked because a write policy is missing: Fix “new row violates row-level security policy”
- New tables returning permission denied after the Oct 30, 2026 grant change (do not “fix” that with
GRANT ALLto anon): Fix “permission denied for table” on new tables - Two-minute checklist (Security Advisor + dangerous patterns): Check your Supabase RLS in two minutes
Prompt for Cursor, Lovable or Bolt
Audit public.<table> for policies with USING (true) or WITH CHECK (true).
Do NOT disable RLS. Do NOT leave always-true write policies for anon or authenticated.
Drop those policies and replace them with (select auth.uid()) = user_id
(USING and WITH CHECK as appropriate). USING (true) is allowed only for
intentional public SELECT on non-sensitive data. Show me the SQL diff first. When to get help
Get help if an AI tool keeps regenerating USING (true) policies, if you have many tables and are not sure which ones should be public, or if Security Advisor is clean but REST still returns other users' rows.
Want us to look at your app?
VibeCPR Rescue Reports are launching soon: we scan your repo and live app, and a human verifies every critical finding. Join the waitlist for early access and founder pricing. You'll also get Patch Notes, our weekly fixes newsletter.
FAQ
What does an RLS policy with USING (true) mean?
USING (true) means the policy allows every row for that operation. Combined with a GRANT to anon or authenticated, anyone holding your public anon key can perform that operation on every row through the Data API. Supabase's Security Advisor flags this as a permissive RLS policy. Replace it with a real condition such as (select auth.uid()) = user_id, unless the table is intentionally public and the policy is SELECT-only.
Is this the same as RLS being disabled?
No. With RLS disabled, grants alone decide access and every granted role can touch every row. With RLS on and USING (true), policies exist but do not restrict rows, so the effect for that operation is the same: full table access for the role. Fix by scoping the policy, not by turning RLS off.
How do I check if my Supabase tables are publicly readable?
Run Security Advisor, then try the REST endpoint for each sensitive table with only your anon key (no user JWT). If private rows come back, the table is publicly readable. Step-by-step checklist: https://vibecpr.com/blog/check-supabase-rls-in-two-minutes
Is my anon key leaked if it is in the frontend?
No. The anon (publishable) key is designed to sit in client code. The leak is data: tables the anon role can read or write because RLS is off or a policy is USING (true) / WITH CHECK (true). Never put the service_role or secret key in the browser.
How does this relate to CVE-2025-48757 and UpGuard's findings?
CVE-2025-48757 covered Lovable apps whose Supabase tables had missing or insufficient RLS, so the public anon key could read or write data. UpGuard's October 14, 2026 study found 16,326 Supabase databases with publicly readable tables across ~300,000 domains, including cases where policies existed but did not adequately restrict access. Always-true policies are one of those inadequate patterns.
Sources
- Supabase docs: Row Level Security
- Supabase docs: Database Advisors (incl. permissive RLS policy)
- UpGuard: Everything Everywhere — systemic data exposure in Supabase apps
- Matt Palmer: statement on CVE-2025-48757
Related fixes
- Fix "Your Cloud Firestore database has insecure rules" (Firebase email)
- Fix Stripe webhook "No signatures found matching the expected signature for payload"
- Fix Supabase Edge Function "blocked by CORS policy" (preflight)
- Fix "new row violates row-level security policy for table" (Supabase)
- Fix Supabase "permission denied for table" (42501) on new tables after Oct 30, 2026
- Fix "404: NOT_FOUND" on refresh in a React/Vite app on Vercel
- Fix "Module not found: Can't resolve" on Vercel (works locally)