Explainer

SECURITY DEFINER functions the anonymous role can call

Sixteen database functions executable by anon across two Supabase apps, one with no caller check. What SECURITY DEFINER means, the two-line fix.

SK Semih Kavaklıoğlu·8 Sept 2026·3 min read Share

Row Level Security is the reason a Supabase app can hand the browser a database key. Every policy says "only the owner sees their rows", and the anonymous key can do nothing else. A SECURITY DEFINER function is the hole in that wall: it runs as its creator, ignores RLS, and by default the anonymous role is allowed to call it. Two of our own apps had sixteen of them.

What does SECURITY DEFINER mean?

Answer. The function runs with the privileges of whoever created it, not of whoever calls it. Inside it, Row Level Security does not apply.

That is sometimes exactly what you want: a function that counts conversations across all users for an admin dashboard, or one that looks up a user id from an e-mail address during an invite flow. The function needs to see rows the caller cannot. So you mark it SECURITY DEFINER, and it works.

The problem is the second half of the story. In a Supabase project, new functions in the public schema are executable by anon and authenticated through the default grants. So the invite helper that looks up a user id by e-mail is callable by anyone on the internet with your public key, which is everyone, because the key is in your JavaScript bundle.

What did the checkups find?

Answer. Sixteen functions across two apps that anon could execute, including one that returns a user id for any e-mail address and one with no check on who was calling.

SECURITY DEFINER functions executable by anon (bar = count)
Established product-photo SaaS
9including get_user_id_by_email and a cleanup routine
Three-day-old customer-messaging SaaS
7one, count_conversations, had no caller check at all
Source: repo security checkups, 7 and 8 September 2026.

Neither app had been breached. Both had RLS on every table and owner-only policies. The functions were the gap: written for one trusted caller, granted to everyone by default, never reviewed because nothing in the normal flow calls them anonymously.

What is the fix?

Answer. Revoke execute from the roles that should not call the function, and check the caller inside functions that must stay callable.

For a function only your server (service role) should call:

revoke execute on function public.get_user_id_by_email(text) from anon, authenticated;

For a function a logged-in user may call, but only about their own data, keep authenticated and check the caller inside:

create or replace function public.count_conversations(p_owner uuid)
returns integer language sql security definer set search_path = public as $$
  select count(*)::int from conversations where owner_id = p_owner and p_owner = auth.uid();
$$;
revoke execute on function public.count_conversations(uuid) from anon;

Two habits stop this from coming back. Set search_path on every definer function (Supabase's advisor flags the ones without it). And change the default so new functions are not born public:

alter default privileges in schema public revoke execute on functions from anon, authenticated;

After that, every function starts private and you grant execute deliberately.

How do you find them?

Answer. One query, or the Supabase security advisor.

select p.proname
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef
  and has_function_privilege('anon', p.oid, 'execute');

An empty result is the goal. Supabase's Database Advisors page lists definer functions without a fixed search_path under security lints; Moonleap's repo checkup runs both and turns each function into a finding your agent can close.

FAQ

Is SECURITY DEFINER itself the mistake?

No. It is the right tool for the few functions that must see across users. The mistake is the default grant, which makes every such function public until someone revokes it.

What about SECURITY INVOKER functions?

They run as the caller and respect RLS, so anon executing them sees nothing it could not see anyway. They are safe by construction unless they do something other than read.

Does the service role need a grant?

No. The service role bypasses grants and RLS; revoking from anon and authenticated does not affect your server.

Sources

  1. PostgreSQL, CREATE FUNCTION, SECURITY DEFINER https://www.postgresql.org/docs/current/sql-createfunction.html
  2. Supabase, Database advisors (security lints) https://supabase.com/docs/guides/database/database-advisors
  3. Supabase, Row Level Security https://supabase.com/docs/guides/database/postgres/row-level-security
  4. Repo security checkups on two of our own Supabase projects, 7 and 8 September 2026 (masked; values never stored)

Share the number

16

SECURITY DEFINER functions executable by the anonymous role across two of our own apps, found in two days

moonleap.io · 8 Sept 2026Post

Related