RowShield
Guides

SECURITY DEFINER views and functions that bypass RLS

A view created with SECURITY DEFINER executes with its owner's privileges. If the owner is a superuser or the table owner, the view returns rows the caller's policies would have denied — and it is reachable through PostgREST like any other relation.

The same applies to a SECURITY DEFINER function granted to `anon` or `authenticated`: it is an RLS bypass with a public entry point.

RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.

Finding SECURITY DEFINER views

Postgres 15 added `security_invoker` on views. A view without it, owned by a privileged role, runs as the definer.

SELECT n.nspname AS schema, c.relname AS view, pg_get_userbyid(c.relowner) AS owner
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'v'
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
  AND COALESCE(
        (SELECT option_value
           FROM pg_options_to_table(c.reloptions)
          WHERE option_name = 'security_invoker'),
        'false') = 'false';

Finding anon-callable SECURITY DEFINER functions

A function with `prosecdef = true` that `anon` can execute is callable by anyone holding your public key.

SELECT n.nspname AS schema, p.proname AS function
FROM pg_catalog.pg_proc p
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
WHERE p.prosecdef
  AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'extensions')
  AND has_function_privilege('anon', p.oid, 'EXECUTE');

Fixing them

For views on Postgres 15 and later, `ALTER VIEW ... SET (security_invoker = true)` makes the view respect the caller's policies.

For functions, either drop SECURITY DEFINER, or `REVOKE EXECUTE ... FROM anon` and keep it server-side. If it genuinely needs elevated privileges, it should not be reachable from a public key.

ALTER VIEW public.your_view SET (security_invoker = true);
REVOKE EXECUTE ON FUNCTION public.your_function FROM anon;

Prioritising what you find

Not every definer object deserves equal urgency. Rank results by three factors: whether anon or authenticated holds EXECUTE (a definer function only superuser can call is inert), what the underlying objects contain (a view over audit logs outranks one over cached lookups), and whether the elevated path crosses tenant boundaries (the combination of BYPASSRLS-equivalent reach plus multi-tenant data is the serious case).

Fix in the same order. Convert views to security_invoker first on Postgres 15 and later — it is a single statement with no behaviour change beyond applying the policies you already wrote. For functions, prefer narrowing grants over rewrites: revoking anon EXECUTE closes the public path today while you decide whether the routine belongs behind an edge function.

Re-run both catalog queries after each change and keep the output; the before-and-after is precisely the evidence an auditor or a future teammate will want. RowShield does not yet automate this class — these queries are the working substitute until function analysis ships. The same discipline applies to grants on the functions themselves: EXECUTE for anon on a definer routine is the loudest signal in this whole class, and revoking it costs nothing while you decide whether the elevation is genuinely warranted. Treat every definer object as a small, documented exception with an owner and a reason, and the population stays auditable; let them accumulate unlabelled and the audit becomes archaeology. Revisit the list quarterly; definer objects arrive quietly with generated helpers and leave loudly only when they fail. Pair each entry with its reason for existing so the next reader inherits judgement rather than mystery.

One further hardening habit costs nothing: prefer SECURITY INVOKER helper functions for policy predicates wherever the logic does not genuinely need elevation. The classic pattern — a definer function guarding recursive membership checks — exists because RLS policies cannot query the table they protect; pinning its search_path and revoking broad EXECUTE keeps that exception small and legible rather than becoming a second, unreviewed authorization layer.

Frequently asked

Does RowShield detect this automatically?
Not yet. The nine shipped rules cover RLS state, policy correctness, storage exposure, index coverage and credential leakage. SECURITY DEFINER analysis is not among them, and this page exists to give you the queries rather than to imply a check that does not run.
Is the Supabase advisor better here?
For this specific check, yes — it flags SECURITY DEFINER views. Use it alongside, and use the queries above between advisor runs.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase security advisor security definer viewsecurity definer view bypass rlsanon callable security definer function supabase