Supabase Security Advisor: what it flags and what it misses
The Advisor is a free, deterministic lint of your database. Run it. Then test as two users, because a table anyone can read through SELECT … USING (true) raises no 0024 warning.
Free scan: GET and HEAD requests only, after you verify you own the domain. The two-account cross-user test is the $29 Proof Report. Updated 25 Oct 2026
In short
- ✓The Advisor is a set of SQL lints you run in Studio, the CLI, MCP or the Management API. Free on every plan (docs, checked 25 Oct 2026).
- ✓It catches real problems: RLS off, views that skip RLS, policies that read
user_metadata, public buckets that list files, andSECURITY DEFINERfunctions callable byanon. - ✓Lint 0024 does not flag
SELECT … USING (true). The lint SQL says so: “SELECT with (true) is often intentional… so we only flag UPDATE/DELETE” (splinter0024_rls_policy_always_true.sql). - ✓Supabase also gives you user impersonation in the Table Editor and an RLS Tester preview that runs SELECT as any role. Use them. They are not a signed-in write test against your live app.
01What Supabase Security Advisor is
Supabase ships three Advisors: Health, Security and Performance. They are deterministic SQL (and, for Health, log) checks. They do not browse your app, do not sign in, and do not send requests as a user. The Advisor docs (checked 25 Oct 2026) list where you can run them:
- Studio: Database → Security Advisor (also Health and Performance).
- CLI:
supabase db advisors. CLI PR 7297 (23 Oct 2026) adds--remotevia the advisors API, so you can lint the hosted project from a terminal. - MCP:
get_advisorswithtypeset tosecurityorperformance. - Management API: the same checks, for agents and CI.
The security lints live in the open-source supabase/splinter repo. Each file is a view that returns rows when a pattern matches. A clean Advisor means those views returned no rows. It does not mean user A cannot read user B’s invoices.
Some findings are intentional. A public blog table should have SELECT … USING (true). A contact-form function should be SECURITY DEFINER so anon can insert one row. The docs say to check each finding against the access model you meant before you change anything.
02The security lints that matter, in plain words
These are the security lints a Lovable, Bolt.new or hand-written Supabase app actually trips. Summaries are from the Advisor docs (checked 25 Oct 2026). Fix SQL is the docs’ own remediation, shortened.
| Lint | What it flags | Fix, in one line |
|---|---|---|
0002 | A view in public that reads auth.users (emails, login methods). Views are SECURITY DEFINER by default, so they skip RLS. | Put a subset in public.profiles with a trigger, or recreate the view with (security_invoker=on) and add RLS. |
0010 | Any SECURITY DEFINER view in an exposed schema. It runs as the owner and ignores RLS on the tables underneath. | alter view public.v set (security_invoker = true); |
0013 | A table in public with RLS off. Anyone holding the public key can read and write every row, if grants allow. | alter table public.t enable row level security; then add policies. RLS on with no policies returns empty, which is the safe default. |
0015 | A policy that reads user_metadata. Users can set their own user_metadata with updateUser({ data }). | Store roles in app_metadata (server-only) or a table only an admin role can write. |
0023 | RLS is off and a column name looks like a password, token, SSN or card number. | Enable RLS. This lint does not fire if RLS is on, even with a loose policy. |
0024 | A permissive policy whose USING or WITH CHECK is literally true / 1=1, on UPDATE, DELETE, INSERT or ALL. See the next section. | Replace true with (select auth.uid()) = user_id. Keep SELECT … USING (true) only on data that is meant to be public. |
0025 | A public storage bucket plus a broad SELECT policy on storage.objects, so clients can list every file. | Drop the listing policy (object URLs still work) or make the bucket private. |
0028 | A SECURITY DEFINER function that anon can EXECUTE. It runs as the owner and skips RLS. Reachable at POST /rest/v1/rpc/…. | revoke execute on function public.f(...) from anon, public; or switch it to SECURITY INVOKER. |
0029 | The same, for authenticated. Revoking only anon leaves every signed-in user able to call it. Open sign-up means anyone. | Revoke from authenticated too, or pin auth.uid() inside the function. |
Related, and easy to mix up: lint 0008 (RLS on, no policy) is the “table is locked shut” warning. That is not a leak. It is the state you want until the owner policies exist. Lint 0011 flags a function whose search_path is mutable; pin it with set search_path = ''.
A green Advisor plus lint 0013 clean means every exposed table has RLS on. It does not mean the policies on those tables keep users apart.
03Why a table anyone can read raises no 0024 warning
This is the claim to verify yourself, because the docs and the lint SQL do not say the same thing.
The Advisor docs page for 0024_permissive_rls_policy describes USING (true) as a problem on SELECT: “Any user with the specified role can read ALL rows.” It lists USING (true), USING (1=1) and a missing USING on a permissive SELECT as detected patterns.
The lint that actually runs is lints/0024_rls_policy_always_true.sql in supabase/splinter (read 25 Oct 2026). The USING check is gated on command:
-- Note: SELECT with (true) is often intentional and documented,
-- so we only flag UPDATE/DELETE
command in ('UPDATE', 'DELETE', 'ALL')
and (
normalized_qual in ('true', '(true)', '1=1', '(1=1)')
or (qual is null and is_permissive)
)
The view’s own description field is explicit: “SELECT policies with USING (true) are intentionally excluded as this pattern is often used deliberately for public read access.”
So this table produces no 0024 row:
alter table public.notes enable row level security;
create policy "anyone can read notes" on public.notes
for select to anon, authenticated
using (true);
RLS is on. Advisor 0013 is silent. Advisor 0024 is silent. A signed-out GET /rest/v1/notes with only the public key returns every row. That is the “green dashboard, open table” case people keep hitting: the switch is on, a leftover USING (true) is still there, and the lint that sounds like it would catch it does not look at SELECT.
0024 does catch the write side: FOR UPDATE USING (true), FOR DELETE USING (true), an INSERT with an empty or true WITH CHECK, and FOR ALL. If your leak is a read, 0024 will not save you.
Two other policies the Advisor will also not call out, because they are not literally true:
using (auth.uid() is not null)orusing (auth.role() = 'authenticated')— every signed-in user sees every row. With open sign-up, that is anyone.using (auth.uid() = author_id)on a table whose owner column isuser_id— the check sits on the wrong column, so it never matches the way you think, or it matches everyone ifauthor_idis null.
Only a request that runs as a second user shows those. List the policies, then test:
select tablename, policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'public'
order by tablename, cmd;
Anything whose qual is true, auth.uid() IS NOT NULL, or a check on user_metadata is a candidate. Then follow How to test Supabase RLS with two users.
04User impersonation and the RLS Tester preview
The Advisor is not Supabase’s only tool. Two others let you act as a user inside Studio. They are the right first check. They are not the same as hitting your deployed API.
User impersonation (public beta, supabase.com/features/user-impersonation, checked 25 Oct 2026) lets you pick a role or a specific user in the Table Editor, SQL Editor and GraphiQL. The grid and the query then obey that user’s RLS. Use it when you want to see, on real rows, what one account gets back. It runs inside the dashboard, as the role you chose, against the project you are already logged into as an owner.
RLS Tester is a feature preview (changelog, 28 Apr 2026; still listed as a preview in the June 2026 developer update). You turn it on from the profile menu. It lets you:
- Run a SQL query as signed out (
anon) or as a signed-in user. - See which RLS policies were evaluated for that query.
- Paste a client-library snippet; the Assistant infers SQL. The changelog says to verify that output.
The same changelog is honest about the limit: only SELECT. INSERT, UPDATE and DELETE are out of the preview because a mutation can fire triggers and Edge Functions. If your leak is a forged insert, a plan = 'pro' patch, or an RPC that writes, the Tester will not show it.
Use both. Then still hit /rest/v1/ with a real JWT from a test account. Impersonation answers “what does this role see in Studio?” The live API answers “what does the key in my shipped JavaScript actually return?” Those diverge when a grant, a security definer function, a storage policy or a view is in the path. The two-user tutorial has the curl.
05What a clean Advisor still cannot tell you
Say this the other way around, so it is not a knock. The Advisor is the right first pass. It is also the thing people treat as a finish line. Here is what a clean run still cannot answer:
- Can user A
GETuser B’s rows on a table whose SELECT policy istrue,auth.uid() is not null, or the wrong column? - Can user A
POSTa row withuser_idset to B? - Can user A
PATCHtheir ownrole,planorcredits? RLS is per row, not per column. - Can user A call
/rest/v1/rpc/…with B’s id and get B’s data back? Lints 0028/0029 catch definer functions that are executable; they do not run the function as A. - Does the deployed app expose a different key, a different schema, or a bucket the Studio lint did not see?
Lovable’s Quick scan covers some of the same configuration (RLS missing or always-true, leaked-password protection) and its Deep scan reads code for cross-user access. Neither signs in as two users against the live app. Aikido’s Lovable product does, at $100 per test launch pricing (checked 25 Oct 2026). You can also do it by hand: two throwaway accounts and the requests in the two-user guide. Preflight’s $29 Proof Report is one more way to run that same test and keep the requests. It is not the only way, and it writes a few marked canary rows on purpose.
06What Preflight checks that the Advisor does not
The free scan does not replace the Advisor. It looks at the published app from outside: the public key in the bundle, row counts signed out, auth settings, bucket list, headers and exposed files. GET and HEAD only, after you prove you own the domain.
The Proof Report signs in as two test accounts you create. It tries cross-user reads on tables, RPCs and storage, a forged-owner insert, and privilege-column changes which it sets back and reads again. It writes marked canary rows on purpose. Leftover canary rows get a one-line DELETE in the report. Other tools run a credentialed cross-user test too; the difference is price, that it works whatever built the app, and a dated report with curl you can re-run. See Compare (checked 25 Oct 2026) and the sample report.
Run the Advisor on every migration. Then run a two-user test on the live app before you share it. A clean lint plus a failed cross-user GET is a real finding, not a disagreement between tools.
?Questions people ask
Is a clean Supabase Security Advisor enough?
No. It means the lints that ran found nothing. Lint 0024 skips SELECT USING (true), and no lint can tell whether auth.uid() = user_id sits on the right column. Test as two users as well.Does Security Advisor flag SELECT USING (true)?
No. The lint SQL in supabase/splinter only flags UPDATE, DELETE and ALL for an always-true USING, plus INSERT WITH CHECK. SELECT is excluded on purpose. Read the policy list yourself.Does the Advisor only check whether RLS is on?
No. It also lints permissive UPDATE/DELETE/INSERT policies, SECURITY DEFINER views and functions, user_metadata in policies, public bucket listing, auth.users exposed through a view, and more. RLS-off is lint 0013, one of many.Can I test as a user inside Supabase?
Yes. Impersonate a user in the Table Editor or SQL Editor, and turn on the RLS Tester preview for SELECT. For writes and for the deployed API, use curl with two JWTs or a tool that signs in as two accounts.Does Preflight replace the Advisor?
No. Run both. The Advisor is free, in-dashboard, and catches configuration. Preflight’s free scan is outside-in; the $29 Proof Report is the two-user test on the live app. Aikido and others offer a two-user test as well.Where do I run supabase db advisors?
In a project with the Supabase CLI. supabase db advisors lints the local database; --remote (CLI PR 7297, 23 Oct 2026) talks to the hosted project through the advisors API.
Advisor is clean?
Check it as two users. Free scan.
The Advisor reads your schema. It never signs in. Preflight’s free scan counts what a stranger can read from outside. The $29 Proof Report signs in as two test accounts you create and tries to read, write and escalate across them. Other tools do a two-user test too; Aikido’s Lovable product is $100 per test (launch pricing, checked 25 Oct 2026).