# SECURITY DEFINER functions in exposed schemas are not executable by anon or authenticated

- **Pattern:** `ab-002644` (`supabase-rls-keys.rls-bypass-surfaces.security-definer-functions-not-public-executable`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-10-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002644
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002644)

## Why it matters

Every function in an exposed schema is a public API endpoint: Supabase serves it at `/rest/v1/rpc/<name>` to anyone holding the anon key. A `SECURITY DEFINER` function runs with its owner's privileges, which bypass RLS, so a definer function that updates a balance, grants a role, deletes a user, or returns rows is an unauthenticated admin endpoint unless EXECUTE is revoked. Postgres grants EXECUTE on new functions to `PUBLIC` by default, and Supabase's default privileges additionally grant it to `anon`, `authenticated`, and `service_role`, so merely writing the function exposes it. AI tools love definer functions ('so RLS does not get in the way') and never revoke execute. Callers pass their own arguments, so a function taking a `user_id` parameter lets any visitor act as any user. The advisor reports this as lints 0028 (anon) and 0029 (authenticated).

## Severity rationale

Critical because an executable SECURITY DEFINER function is an RLS-bypassing endpoint callable with the public anon key, so a single missed revoke can hand out reads or writes across every user.

## Remediation

Either move the function out of the exposed schema or take EXECUTE away from the API roles. Revoke from `public` too, because Postgres grants it to `PUBLIC` by default, and from `anon` and `authenticated` explicitly because Supabase grants those by name:

```sql
revoke execute on function public.grant_credits(uuid, int) from public, anon, authenticated;
grant execute on function public.grant_credits(uuid, int) to service_role;
```

Better, create helper and admin functions in a `private` schema that is not exposed. Always give definer functions a fixed search path (`set search_path = ''`) and have them read the caller from `auth.uid()` rather than a parameter. If an RPC is meant to be callable, add a `-- INTENTIONALLY_CALLABLE: <reason>` comment directly above `CREATE FUNCTION`.

## Detection

**The detection method for this pattern is part of AuditBuffet Pro.**

The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the [Supabase RLS & Keys audit](https://auditbuffet.com/audits/supabase-rls-keys). The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.

## External references

- cwe CWE-284
- cwe CWE-285
- cwe CWE-269
- owasp A01
- external 0028_anon_security_definer_function_executable — https://supabase.com/docs/guides/database/database-advisors?lint=0028_anon_security_definer_function_executable
- external 0029_authenticated_security_definer_function_executable — https://supabase.com/docs/guides/database/database-advisors?lint=0029_authenticated_security_definer_function_executable

Taxons: access-control, abuse-resistance

HTML version: https://auditbuffet.com/patterns/ab-002644
