Kurz gesagt
Eine Postgres-View läuft mit den Rechten ihres Eigentümers, es sei denn, sie wird mit security_invoker = true angelegt. In Supabase ist der Eigentümer meist postgres, und der umgeht RLS: Eine View über einer geschützten Tabelle liefert also Zeilen, die die Tabelle selbst verbirgt. Auf meinem lokalen Stack bekam der öffentliche Key 0 Zeilen aus der Tabelle und alle 3 von 3 aus einer einfachen View darüber; mit security_invoker = true lieferte die View ebenfalls 0. Ab Postgres 15 setzen Sie es auf jeder View in einem exponierten Schema.
Mein kostenloser Supabase-Sicherheitscheck hat eine Regel namens “Views, die RLS umgehen”, und der Security Advisor von Supabase meldet dasselbe als security_definer_view. Man übersieht es leicht, denn die Tabelle, aus der es leckt, sieht geschützt aus.
Warum umgeht eine Supabase-View RLS?
Eine View ist eine gespeicherte Abfrage. Standardmäßig prüft Postgres Rechte und RLS der Tabellen darin als Eigentümer der View, nicht als die Person, die sie aufruft. Der RLS-Leitfaden von Supabase sagt es direkt: Views umgehen RLS standardmäßig, weil sie meist mit dem Nutzer postgres angelegt werden, und eine View über einer geschützten Tabelle gibt jede Zeile heraus, die ihre Policies zurückhalten sollten.
Die Data API liefert Views wie Tabellen aus. Liegt die View in einem exponierten Schema und dürfen anon oder authenticated sie lesen, liest sie der öffentliche Key.
Was lieferte die View in meinem Test?
Auf einem lokalen Supabase-Stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) legte ich eine Tabelle orders mit drei erfundenen Kunden an, aktivierte RLS ohne Policy und legte darüber eine View order_summary mit einem select-Grant für anon an. Dann rief ich beide mit dem publishable key ab:
| Anfrage mit dem publishable key | Ergebnis |
|---|---|
GET /rest/v1/orders (RLS an, keine Policy) |
[] |
GET /rest/v1/order_summary (einfache View) |
alle 3 Zeilen, mit Namen und E-Mails |
GET /rest/v1/order_summary nach security_invoker = true |
[] |
Dieselbe Tabelle, derselbe Key. Nur die Einstellung der View änderte, was zurückkam.
Wie beheben Sie das?
Ab Postgres 15, das jedes aktuelle Supabase-Projekt nutzt, lassen Sie die View RLS als Aufrufer prüfen:
alter view public.order_summary set (security_invoker = true);
Für eine neue View gehört es in die Definition:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Der Aufrufer braucht dann auch select auf der zugrunde liegenden Tabelle, und deren Policies entscheiden über die Zeilen. Muss eine View Daten aggregieren, die der Aufrufer nicht zeilenweise sehen darf, etwa eine öffentliche Zahl, halten Sie sie aus dem exponierten Schema heraus und liefern sie stattdessen über eine Funktion mit schmalem Ergebnis aus.
So listen Sie die Views in public auf, die noch als ihr Eigentümer laufen:
select c.relname as view
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
and not coalesce(
(select bool_or(option = 'security_invoker=true' or option = 'security_invoker=on')
from unnest(c.reloptions) as option), false);
Auf meinem Stack, mit einer einfachen View und zwei mit security_invoker = true, lieferte sie nur die einfache zurück.
Materialized Views unterstützen RLS gar nicht, deshalb meldet der Check sie getrennt unter “Materialized Views oder Foreign Tables in der API”. Was sonst noch an Policies vorbeigeht, vom secret key bis zu SECURITY DEFINER-Funktionen, steht in meiner Sicherheits-Checkliste für Lovable-Apps, und wie Sie die Policies selbst beweisen, in Supabase RLS testen. Eine Policy, die Teams über eine Hilfsfunktion ohne Rekursion nachschlägt, steht in Supabase RLS Policy Beispiele, je per API getestet. Eine Security-Definer-Funktion braucht noch eine Einstellung, einen festen search_path; warum search_path = public in meinem Test nicht reichte, steht in Supabase Function Search Path Mutable: was die Warnung heißt.