buildwiththis home

What Supabase’s Security Advisor doesn’t flag: insert-only and column-level grants

Short answer: Security Advisor doesn't flag every open table. RLS (row level security) is the per-row access rule on each table. In our test, its "RLS Disabled in Public" check flagged 2 of 4 reachable RLS-off tables. It only lists tables that anon or authenticated can SELECT whole, so it doesn't flag an insert-only grant or a column-level select grant. On a hosted Supabase project, a logged-out client inserted into one and read the other, and neither was flagged.

Tested Oct 25, 2026, on hosted Supabase and Postgres 17 with Supabase's splinter linterFinding, not a tool verdictRe-test due by Nov 30, 2026

Free: the 10-check security checklist from our scanner test. Download the PDF, no signup. Or get an email when it changes.

The Advisor is working as designed. A lint is one automated check. This one asks one question: "can a client role read this RLS-off table?" It answers that correctly. A role is who Supabase thinks is asking, and a grant lets a role use a table. Write-only and column-only access are outside that question. Below is how to reproduce it, why the lint skips these cases, and one query that covers them. For the warning itself and the full fix, see our "RLS Disabled in Public" guide.

Advisor output vs our query

Six empty demo tables, each with one kind of access. Same result on both environments.

TableSetupSecurity AdvisorOur queryLogged-out request
anon_select_demoRLS off, select to anonFlagged (ERROR)ListedRead: 200
auth_select_demoRLS off, select to authenticatedFlagged (ERROR)ListedRead: 401 (logged out)
anon_insert_only_demoRLS off, insert to anon onlyNot flaggedListedInsert: 201, row written
anon_column_select_demoRLS off, select (id) to anonNot flaggedListedRead ?select=id: 200 with the row
no_grants_demoRLS off, no grantsNot flaggedNot listedRead: 401 42501
rls_on_demoRLS on, select to anon, no policyNot flagged by 0013 (0008 INFO: no policy)Not listedNot requested

Of the four RLS-off tables a client role could reach, the Advisor flagged two and our query listed all four. Both skipped the table with no grants and the table with RLS on.

EnvironmentAdvisor side"RLS Disabled in Public"Our query
Hosted Supabase project (Free plan, Postgres 17.11), Oct 24 and Oct 25Security Advisor lints via Supabase’s MCP get_advisors3 flagged*5 listed*
Plain Postgres 17.11 with the anon and authenticated roles, Oct 25Supabase’s open-source linter (splinter) SQL, run in full2 flagged4 listed

* The hosted project also had one RLS-off table from an earlier test, with select granted to anon. Both the Advisor and our query listed it. Plain Postgres had only the six demo tables.

The setup

Run this on a throwaway project, or a local database where you've created the anon and authenticated roles. It's the core of our fixtures. The full file is in our test log.

-- Run on a throwaway project or database. Empty demo tables only.
create table public.anon_select_demo        (id int primary key, body text);
create table public.anon_insert_only_demo   (id int primary key, body text);
create table public.anon_column_select_demo (id int primary key, secret_note text);
create table public.no_grants_demo          (id int primary key, body text);

-- Start from zero grants, then give each table one kind of access
revoke all on public.anon_select_demo, public.anon_insert_only_demo,
  public.anon_column_select_demo, public.no_grants_demo from anon, authenticated;

grant select on public.anon_select_demo to anon;              -- whole-table read
grant insert on public.anon_insert_only_demo to anon;         -- write, no read
grant select (id) on public.anon_column_select_demo to anon;  -- one column

Then run Security Advisor. In the Supabase dashboard, open Advisors, then Security Advisor. It flags anon_select_demo as "RLS Disabled in Public" and nothing else from this set.

Now try the two tables it didn't flag, logged out:

# Logged out, publishable key, your own project only
curl -X POST "https://<project-ref>.supabase.co/rest/v1/anon_insert_only_demo" \
  -H "apikey: $PUBLISHABLE_KEY" -H "Content-Type: application/json" \
  -H "Prefer: return=minimal" -d '{"id": 1, "body": "written while logged out"}'

curl "https://<project-ref>.supabase.co/rest/v1/anon_column_select_demo?select=id" \
  -H "apikey: $PUBLISHABLE_KEY"

BuildWithThis check. Tested Oct 25, 2026 on our hosted lab project. The insert returned 201 and the row was in the table afterwards. The column read returned [{"id":1}]. Asking for any other column returned 401 42501, so the grant limits which columns, not which rows.

The query that catches it

In the SQL Editor, run the query below. It's the same one as in our RLS guide.

-- Public tables with RLS off that anon or authenticated can reach
select c.relname as table_name,
       has_table_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
         or has_any_column_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE') as anon_can_reach,
       has_table_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
         or has_any_column_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE') as authenticated_can_reach
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind in ('r', 'p')
  and not c.relrowsecurity
  and (
    has_table_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
    or has_any_column_privilege('anon', c.oid, 'SELECT,INSERT,UPDATE')
    or has_table_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE,DELETE')
    or has_any_column_privilege('authenticated', c.oid, 'SELECT,INSERT,UPDATE')
  )
order by 1;

This is a BuildWithThis query built on the Postgres catalog, not a Supabase snippet. Tested Oct 24 and 25, 2026 on both environments. It listed every table the Advisor flagged, plus the insert-only and column-grant tables, and skipped the no-grant and RLS-on tables. It checks public only. Change n.nspname = 'public' if you expose other schemas.

Why “RLS Disabled in Public” doesn’t flag them

Security Advisor runs Supabase's open-source linter, splinter. Here is the filter for "RLS Disabled in Public", from the lint 0013 source at the commit of Sep 29, 2026:

-- splinter lints/0013_rls_disabled_in_public.sql (lines 31-37)
c.relkind = 'r' -- regular tables
and not c.relrowsecurity
and (
    pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')
    or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')
)

Insert-only: the lint only asks about SELECT

A table where anon holds insert but not select fails that test, so it isn't listed. We checked every splinter lint that tests table privileges (0002, 0010, 0013, 0016, 0017, 0019, 0023, 0026, 0027). All of them test SELECT, and none tests INSERT, UPDATE or DELETE. So with RLS off, anyone with your publishable key can write rows into such a table, and no Advisor lint reports it.

Column grant: has_table_privilege doesn't count column grants

grant select (id) on … to anon is a column-level grant. Postgres documents has_any_column_privilege as succeeding "if the privilege is held for the whole table, or if there is a column-level grant of the privilege for at least one column". has_table_privilege checks the whole table only. Both are in the Postgres 17 docs, 9.27.2. On both environments, has_table_privilege('anon', …, 'SELECT') returned false for the column-grant table while has_any_column_privilege returned true. The lint uses the first, so the table isn't listed. But logged-out visitors can still read every row of the granted columns.

One related lint does see column grants

Lint 0026, "Public Can See Object in GraphQL Schema" (WARN), checks each column with has_column_privilege. A comment in the lint 0026 source says that's so column-level grants "are also caught". It only runs when the pg_graphql extension is installed. Our lab project didn't have it, so it didn't fire for us. If your project has pg_graphql, you may see the column-grant table there, as a GraphQL finding, not an RLS one. It doesn't cover insert-only grants.

The fix

It's the same as for any RLS-off table. Turn RLS on, and keep only the grants your app uses.

-- Turn RLS on. With no policies, anon can't write or read rows.
alter table public.anon_insert_only_demo enable row level security;
alter table public.anon_column_select_demo enable row level security;

-- Take back any grant the app doesn't use
revoke insert on public.anon_insert_only_demo from anon;
revoke select (id) on public.anon_column_select_demo from anon;

BuildWithThis, built on Supabase's RLS docs. Tested Oct 25, 2026 on the hosted project. With RLS on, the logged-out insert returned 401 42501 "new row violates row-level security policy", and the Advisor moved both tables to "RLS Enabled No Policy" (INFO). On plain Postgres, the whole block ran and the two grants were gone afterwards. Our query then listed neither table.

Logged-out inserts may be intentional, as in a contact form. If so, keep the grant, turn RLS on and add an insert policy with a with check that limits what can be written. A policy is a rule that says who may see or change which rows. The full pattern, with one policy per operation and a pgTAP test, is in the "RLS Disabled in Public" fix guide.

What we tested

  • Hosted: Oct 24, 2026 at about 4:25 AM ET, and again Oct 25, 2026 at about 3:55 AM ET. We used a throwaway Supabase lab project on the Free plan with Postgres 17.11. We read Advisor results through get_advisors on Supabase's MCP server, a connector that lets AI tools talk to your project. It runs the same lints the dashboard shows. We sent logged-out requests to the REST API with the publishable key. Logged-out requests to the REST API The demo tables were dropped afterwards.
  • Independent: Oct 25, 2026, on plain Postgres 17.11 without Supabase. We created the two client roles by hand and ran splinter's full SQL from the Sep 29, 2026 commit. There was no REST layer, so the access checks ran as set role anon. The insert succeeded, and reading the granted column worked while select * was refused.

We didn't test the dashboard Advisor screen itself, supabase db advisors, the Supabase CLI's local stack, projects with pg_graphql installed, or schemas other than public. We also didn't test update-only or delete-only grants. The lint source skips them for the same reason as insert-only.

Tested on: Oct 24–25, 2026 · Supabase Free plan · Postgres 17.11 · splinter commit fccca4b. Re-test due Nov 30, 2026. Written and fact-checked by AI agents. Every result here is in our test log, and every quote links to its source.

Sources

Related: Supabase "RLS Disabled in Public" fix guide · Supabase 42501 fix guide · vibe-coding security checklist

General guidance from small test setups, not a security audit. Check only projects you own.