Node2Shell (CVE-2026-111497) affects Node.js: read how to fix it

Guides

How to check Supabase Row Level Security (RLS)

Written by our founder, a cybersecurity architect · Updated

Short answer

Run the query below in the Supabase SQL editor: any table in public with rls_enabled = false can be read and changed by anyone who has your anon key, and your site gives that key to every visitor.

Then read the policies on the tables that have RLS, because a policy like using (true) lets everyone through. Finally, test with the anon key from outside your app: if a request with no user returns rows, so will an attacker’s.

Step 1: Which tables have RLS turned off?

SQL editor
select tablename, rowsecurity as rls_enabled
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Every row with false needs fixing. Turning RLS on with no policy blocks all access through the API, which is safe but may break your app until you add policies:

alter table public.orders enable row level security;

The Security Advisor (Advisors → Security Advisor) shows the same problem as “RLS disabled in public”, along with other warnings worth reading.

Step 2: What do your policies allow?

SQL editor
select tablename, policyname, cmd, roles, qual as using_expr, with_check
from pg_policies
where schemaname = 'public'
order by tablename;

Look for these patterns:

  • using_expr is true and roles includes anon or public: anyone can read the table. Fine for a list of blog posts, not for users or orders.
  • roles is authenticated and the expression doesn’t mention auth.uid(): any signed-in user can read every row. With open sign-ups, that is anyone.
  • cmd is INSERT or UPDATE and with_check is empty: users can write rows with someone else’s user_id.
  • An ownership check on a column the user can change, like user_id in an update without with check.

Step 3: How do I limit rows to their owner?

For a table where each row belongs to one user:

create policy "Read own orders" on public.orders
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "Create own orders" on public.orders
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

Writing (select auth.uid()) instead of auth.uid() lets Postgres evaluate it once per query instead of once per row, which matters on large tables. Add an index on user_id for the same reason.

Step 4: What can a stranger read?

The SQL editor runs as an admin and ignores RLS, so it can’t tell you what a visitor sees. Use the API with only your public key, from a terminal:

Terminal
curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
  -H "apikey: YOUR_ANON_OR_PUBLISHABLE_KEY"

An empty list [] means a visitor with no account sees nothing. Any row in the answer means anyone can read that table. Test each table that holds personal data, and test again signed in as a second user to check they can’t see the first user’s rows.

What doesn’t RLS cover?

  • The service_role (or secret) key ignores RLS. It must never be in frontend code. If it was, rotate it.
  • Views run with the permissions of their owner by default, so a view over a protected table can expose it. Create views with with (security_invoker = true), or keep them out of the public schema.
  • Functions marked security definer in the public schema can be called through the API and also skip RLS. Check who can execute them.
  • Storage has its own policies, on storage.objects, and buckets marked public skip them for downloads.

Can I check RLS automatically?

Policies drift as your app changes: a new table added by your AI tool can arrive without RLS. Our code scan flags migrations that create tables without RLS on every pull request, and our backend checks test which tables a visitor with no account can read on your live site, counting rows without downloading them. Both are included in the free plan, with the backend check once a month (weekly on Pro), and backend checks run only after you prove you own the site.

Checklist

  • Every table in public has RLS on
  • No using (true) on tables with personal data
  • Policies for authenticated check auth.uid()
  • Insert and update policies have with check
  • A request with only the anon key returns [] for private tables
  • A second user can’t read the first user’s rows
  • Views use security_invoker, and security definer functions are reviewed
  • The service_role key is only on the server

Scan your site for free

Leaked API keys, exposed files and missing security headers, with a fix for each. No account, about a minute.

Free, no signup needed

Questions

Is RLS enabled by default in Supabase?
Tables created in the Table Editor have RLS on by default, but tables created with SQL, including migrations written by AI tools, may not. Check with the query in step 1.
What happens if RLS is off on a Supabase table?
Anyone who has your project URL and anon key can read, insert, update and delete rows in that table through the API. Both values are in your site’s JavaScript.
Does the service_role key bypass RLS?
Yes. It is meant for trusted servers only. If it is in your frontend, anyone can read and change your whole database.
Why does my query return rows in the SQL editor but not in my app?
The SQL editor runs as an admin and ignores RLS. Your app uses the anon key or a user session, so it only sees what your policies allow.

Related guides

How to check Supabase Row Level Security (RLS) · Afterprompt