Supabase SECURITY DEFINER functions: the risk and the fix
Short answer
A SECURITY DEFINER function runs with its owner's privileges, which on Supabase is usually a role that bypasses RLS. By default, functions in public are executable by anon and authenticated, so anyone with your public key can call them at /rest/v1/rpc/<name>. Supabase flags this as lints 0028 and 0029. The fix: revoke EXECUTE, switch to SECURITY INVOKER, or move the function to a schema the API doesn't expose.
Find them
select p.proname,
pg_get_function_identity_arguments(p.oid) as args,
pg_catalog.pg_get_userbyid(p.proowner) as owner,
has_function_privilege('anon', p.oid, 'execute') as anon_can_call
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef
order by p.proname;The fix
Pick one per function:
-- A. Nobody outside the server should call it
revoke execute on function public.my_priv_op(int, text) from anon, authenticated, public;
-- B. It doesn't actually need elevated rights
alter function public.my_priv_op(int, text) security invoker;
-- C. Stop future functions being auto-executable
alter default privileges in schema public
revoke execute on functions from anon, authenticated, public;
If a function must stay definer (for example, a helper used inside a policy), keep it in a non-exposed schema such as private, pin set search_path = '', fully qualify every name, and grant execute only to the role that needs it.
create function private.has_role(r text) returns boolean
language sql security definer set search_path = '' stable as $$
select exists (select 1 from public.user_roles
where user_id = (select auth.uid()) and role = r);
$$;
revoke execute on function private.has_role(text) from public;
grant usage on schema private to authenticated;
grant execute on function private.has_role(text) to authenticated;How to check
- Security Advisor should no longer show 0028/0029 (or 0011 "function search path mutable") for the function.
- Re-run the query above:
anon_can_callshould be false for anything privileged. - If a function is intentionally public (a contact form insert), make sure it validates input and only does that one thing.
Check your own project in seconds
Upload a .zip of your own Lovable, Bolt, Cursor, Supabase or Firebase project. ProdCanary reviews the code statically and shows your top 3 risks with plain-English fixes. Your upload is deleted right after the scan.
Run a free scanFAQ
Why does revoking from anon alone not fix it?
Postgres grants EXECUTE to PUBLIC by default and Supabase also grants authenticated. Revoke from anon, authenticated and public together, as Supabase's lint docs recommend.
Are views affected too?
Yes. Views default to security definer behaviour and bypass RLS. Use with (security_invoker = true) on Postgres 15+ (lint 0010).
Is every security definer function a bug?
No. It's a real risk when the function is in an exposed schema and callable by anon or authenticated without validating input.
Sources
- Supabase Advisors: 0028 anon security definer function executable
- Supabase: Row Level Security (security definer functions)
This guide is for securing apps you own or are authorised to work on. Facts above are from the official docs linked; check them for changes.