RowShield
Guides

Views that execute with owner rights and skip policy checks

A view is stored SQL with its own owner. By default it executes with that owner's rights, which means the row level security on the tables beneath it never engages for the caller — the view sees everything, and PostgREST happily serves the view.

Postgres 15 added security_invoker to flip this: the view then executes as the calling role, and underlying policies apply. Projects predating the flag — or generated code unaware of it — carry views that silently bypass the entire policy layer. RowShield does not yet automate view analysis (roadmap, stated plainly); the companion guide security-definer-view-bypass covers the definer-function twin of this problem.

The conversion itself is one statement per view, which makes this among the cheapest full-layer fixes available: unlike policy redesigns, nothing about the predicate work is ambiguous once the flag is set.

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.

What actually happens

Caller asks PostgREST for the view. Postgres plans the view under its owner's authority; underlying-table RLS is skipped for owner access. Rows the anon key could never fetch directly flow through the view with a 200.

The trap deepens with joins: a "safe projection" view over two tables exposes the union of both tables' contents as seen by the owner — often everything.

Internally, a view is stored SQL plus an owner: pg_class.relowner records whose rights apply, and during planning the view query is spliced into yours with permission checks performed as that owner. Under the pre-15 default the caller's policies on base tables never engage, because as far as those checks are concerned the reader is the owner. Chained views mix treatments layer by layer: each view applies its own flag to its own expansion, so an invoker view over a default view still routes the inner selection through the inner owner.

Finding unconverted views

reloptions carries the security_invoker flag; absence is the finding. Owner privilege relative to your tables tells you how severe the bypass is:

Run the query in every environment: staging accumulates experimental views that never shipped but still hold real data, and they bypass policies on the same terms production views would.

SELECT n.nspname AS schema, c.relname AS view_name,
       pg_get_userbyid(c.relowner) AS owner,
       COALESCE(
         (SELECT option_value FROM pg_options_to_table(c.reloptions)
          WHERE option_name = 'security_invoker'),
         'false') AS security_invoker
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');

Converting and verifying

Flip the flag, then exercise the view through PostgREST as the anon caller — behaviour, not definition, is the proof:

Convert chained views in dependency order, deepest first, so behavioural verification exercises the final arrangement rather than a halfway state.

ALTER VIEW public.active_members
  SET (security_invoker = true);

-- Verify behaviourally afterwards:
-- curl "$SUPABASE_URL/rest/v1/active_members?select=*" \
--   -H "apikey: $SUPABASE_ANON_KEY"

Coverage honesty

View analysis is a roadmap rule with fixtures drafted and no promised date. Today's shipped rules cover the nine areas listed on the rules index; the advisor detects definer-view cases and pairs well with the conversion discipline here. Continuous scanning complements both by catching the day a new unconverted view appears in a generated migration.

Variations you will meet

Materialised views are the harder case: they have no security_invoker option at all and always serve their stored snapshot under the owner's authority, so conversion is unavailable — restrict grants on them or rebuild as plain views. Extensions install views of their own; exclude extension schemas from the audit rather than chasing false positives, but include views left behind by one-off dashboards and departed contractors, which nobody remembers owning.

Upgrades do not convert anything retroactively either: a project moved onto Postgres 15 keeps default semantics on every view created earlier, so age, not intent, predicts the inventory. The oldest views in the catalog deserve the first look.

Common false leads

Converting the flag fixes the view, not its neighbours. If the view calls a SECURITY DEFINER function with an unpinned search_path, the elevation simply moves one layer down. Removing the caller's grants on underlying tables changes nothing either — under default semantics the owner's rights govern the expansion, so the view serves regardless of what the caller could reach directly. Verification has to be behavioural, through the endpoint and with the real keys.

Frequently asked

Is this different from SECURITY DEFINER views?
Related but distinct: definer views elevate execution to their owner deliberately, while ordinary views default to owner-rights evaluation without anyone choosing it. Both skip the caller's policies on base tables, and security_invoker fixes the ordinary case on Postgres 15 and later.
Any downsides to security_invoker?
Performance can shift: policies now apply inside view expansion, and scans that previously ran without predicates gain them. Measure the hot views after converting, index what the policies reference, and weigh the cost against the leak the default was causing.
Does the free audit check views?
Not yet — the probe covers tables and bundle-exposed keys, and view analysis remains a roadmap rule with fixtures drafted. The catalog query on this page fills the gap today, and the advisor detects definer-view cases alongside it.

Check your project in about ten seconds

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

Run the free audit
supabase view security_invokerview bypasses rls postgresalter view security invokerpostgres 15 security invoker view