SECURITY DEFINER functions: the Supabase RLS bypass hiding in your RPC endpoints
Shaig Khaligli, security engineer
· 4 min read
A SECURITY DEFINER function runs with the privileges of whoever created it, usually postgres, and ignores Row Level Security. If that function lives in an exposed schema like public, anyone with your publishable key can call it over the API. Supabase's docs say never to create one there.
It's one of the easiest ways to undo good RLS policies without noticing, because the table policies all look correct. The leak is in the function.
Key takeaways
- Every function in an exposed schema is callable at
/rest/v1/rpc/function_name.SECURITY DEFINERfunctions run with their owner's rights and skip RLS, so they need their own access checks.- Supabase recommends the default,
security invoker, and says never to put a definer function in an exposed schema.- If you need definer rights, use a private schema, an empty
search_pathand revokedEXECUTEgrants.
Invoker vs definer at a glance
SECURITY DEFINER is a Postgres setting that makes a function run with its owner's privileges instead of the caller's. SECURITY INVOKER, the default, runs with the caller's privileges.
security invoker (default) |
security definer |
|
|---|---|---|
| Runs as | The user calling it | The function's owner, often postgres |
| RLS on tables it reads | Applies as normal | Skipped |
Safe in public? |
Yes, policies still protect data | No, Supabase says never put it in an exposed schema |
| When to use | Almost always | Lookups a policy needs that the caller can't read directly |
Why these functions exist in the first place
Usually for a good reason. RLS policies sometimes need to look something up that the current user isn't allowed to read directly, like checking team membership in a table that's itself protected. A definer function can do that lookup with elevated rights and return a yes or no.
The problem is that the same mechanism works for anything. A function written to "get a user's profile by id" with SECURITY DEFINER will happily return any user's profile to whoever asks, because RLS on profiles never gets a say.
What the docs actually recommend
Supabase's database functions guide says to "prefer security invoker, which is also the default" (Supabase docs, Database functions). An invoker function runs as the caller, so the caller's RLS policies apply as usual.
The RLS guide is more direct about the risk: "A security definer function in an exposed schema is callable over the Data API with the creator's privileges. Never create one in a schema listed under 'Exposed schemas' in your API settings" (Supabase docs, Row Level Security).
PostgreSQL's own documentation adds the search_path warning: because a definer function runs with its owner's privileges, "search_path should be set to exclude any schemas writable by untrusted users" (PostgreSQL docs, CREATE FUNCTION). Supabase's examples go one step further and set it to an empty string, with every table name fully qualified.
How to find the risky ones
This lists every definer function in schemas your API exposes by default:
select n.nspname as schema, p.proname as function
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where p.prosecdef
and n.nspname in ('public', 'graphql_public');
If your project exposes other schemas, add them to the list. Every row that comes back is a function any visitor with your publishable key may be able to call.
The Security Advisor in your dashboard (under Advisors) also flags definer functions that are callable by the anon and authenticated roles, alongside other checks like tables with RLS disabled (Supabase docs, Database advisors). Our free exposure checker lists the functions your API exposes, too.
Fixing them
For each function the query found, ask one question: does it actually need to bypass RLS?
Most don't. Switch them to the default:
alter function public.get_my_orders() security invoker;
If it genuinely needs definer rights, move it out of the exposed schema, pin search_path, check the caller inside the function, and remove execute rights from roles that shouldn't have them:
create schema if not exists private;
create or replace function private.is_team_member(team uuid)
returns boolean
language sql
security definer
set search_path = ''
as $
select exists (
select 1 from public.memberships
where team_id = team and user_id = (select auth.uid())
);
$;
revoke execute on function private.is_team_member(uuid) from public, anon;
A policy on another table can then call private.is_team_member(team_id). The API can't call it directly, because private isn't exposed.
Then test it. Sign in as a second test user and call the function with the first user's ids. If you get their data back, it's still open.
Check the rest while you're there
Definer functions are one item on our Supabase RLS checklist, next to views that bypass RLS by default and write policies missing WITH CHECK. And if you're still unsure about what your public key can reach, start with whether the anon key is safe to expose.
If you'd rather have someone go through every function and policy by hand, that's exactly what our premium scan does.