Built and run by one person.

Can a SECURITY DEFINER function bypass my row-level security?

Yes - completely, and by design. A Postgres function marked SECURITY DEFINER executes with the function owner's privileges, which means it bypasses row-level security entirely. Your policies are simply not consulted.

The dangerous shape is a SECURITY DEFINER function that accepts a user-supplied identity parameter. Consider increment_usage(p_email text, ...): any authenticated user can call it with someone else's email and insert or update usage records for that person. That is complete identity spoofing, and RLS cannot prevent it - the function was handed the identity rather than deriving it.

The fixes, in order. First: custom functions almost never need SECURITY DEFINER - use the default SECURITY INVOKER and let the caller's own privileges (and your policies) apply. Second: if a function genuinely needs elevated privileges, never accept identity as a parameter. Pull it from the JWT inside the function body with auth.email() or auth.uid(), which the caller cannot forge.

The gotcha when you fix one. Postgres treats different parameter counts as separate overloads. Deploying the corrected function does not replace the vulnerable one - the old signature is still there and still callable. You must DROP FUNCTION the old version explicitly, with its full parameter list. It is entirely possible to "fix" this, verify the new function works, and leave the hole wide open.

How to audit: query pg_proc for prosecdef = true, excluding pg_catalog and information_schema. Supabase's own system functions (graphql, pgbouncer, pgsodium, vault, storage) use SECURITY DEFINER legitimately and are safe - focus on custom functions in the public schema.

Related: why enabling RLS is not enough on its own. Full item with the audit query: https://www.tigzig.com/security/database.

← All Agents FAQ