Sicherheitsgrundlagen
"Infinite recursion detected in policy" ohne RLS abzuschalten
"Infinite recursion detected in policy for relation" heißt: Deine Policy hat die Tabelle gefragt, die sie schützt. So brichst du den Kreis.

Kurz gesagt
- "Infinite recursion detected in policy for relation" ist der Postgres-Fehler 42P17. Eine Row-Level-Security-Regel auf einer Tabelle hat genau dieser Tabelle eine Frage gestellt, und die Frage hat kein Ende.
- Das ist kein Zeichen dafür, dass Row Level Security falsch für deine App war. Es ist ein Zeichen dafür, dass eine Regel im Kreis gefragt hat.
- Eine kleine Hilfsfunktion liest die Tabelle außerhalb der Regeln und bricht den Kreis. Row Level Security auf dieser Tabelle abzuschalten beendet den Fehler ebenfalls, indem es jede Zeile darin an jeden weitergibt, der den Schlüssel hat, der in deiner App mitgeliefert wird.
Du hast Row Level Security angeschaltet, eine Regel geschrieben, damit Admins jedes Profil sehen und alle anderen nur ihr eigenes, und jetzt lädt kein einziger Bildschirm in deiner App mehr. Jeder Lesezugriff kommt mit derselben Zeile zurück:
infinite recursion detected in policy for relation "profiles"
Die meisten Antworten, die du findest, sagen dasselbe, und es ist das Falsche: schalte Row Level Security auf dieser Tabelle ab, bis du es herausgefunden hast. Der Fehler hört tatsächlich auf. Er hört auf, weil die Tabelle jetzt für jeden lesbar ist, der deine Seite besucht.
Was "infinite recursion detected in policy for relation" bedeutet
Eine Regel auf einer Tabelle musste genau diese Tabelle lesen, bevor sie antworten konnte.
Stell dir einen Türsteher vor, der jeden Besucher gegen eine Gästeliste prüft, und die Gästeliste liegt in dem Raum, den er bewacht. Um die Liste zu lesen, muss er hinein. Um hineinzugehen, muss er die Liste prüfen. Es gibt keinen ersten Schritt, also steht er im Flur und niemand kommt irgendwohin.
Genau das ist deiner Datenbank passiert. Sie wurde nach ein paar Zeilen aus
profiles gefragt, ging zur Regel auf profiles, um herauszufinden, welche du
sehen darfst, und die Regel schickte sie nach profiles. Postgres merkt, dass
es im Kreis läuft, gibt auf und meldet den Fehler mit dem Code 42P17 daran.
Supabase reicht ihn unverändert an deine App weiter, deshalb liest er sich wie
Maschinerie.
Die Abfrage scheitert für alle, für die die Regel gilt, also kommt das üblicherweise als eine ganze App an, die auf einmal leer bleibt, statt als ein einzelner kaputter Bildschirm.
Der Kreis, gezeichnet
Die Regel, die das auslöst, ist die, die fast jeder zuerst schreibt, weil sie genau das sagt, was du meinst:
-- Falsch: das liest profiles, um zu entscheiden, wer profiles lesen darf.
create policy "admins read every profile" on profiles for select
to authenticated
using (
exists (
select 1 from profiles
where id = (select auth.uid()) and role = 'admin'
)
);
Das exists (select 1 from profiles …) ist das ganze Problem. profiles zu
lesen heißt, die Regel auf profiles anzuwenden, und die führt exists (select 1 from profiles …) erneut aus.
Supabases eigener Row-Level-Security-Leitfaden nennt die Variante mit zwei Tabellen: Policies auf zwei Tabellen, die jeweils die andere lesen, lösen sich nie auf, und die Abfrage scheitert für jede Rolle, für die die Policies gelten. Eine Tabelle, die sich selbst liest, ist dieselbe Form ohne den Umweg.
Warum passiert das immer an der profiles-Tabelle?
Weil in profiles steht, wer diese Person ist.
Jede Regel, die eine Sorte Mensch anders behandelt als eine andere, muss
herausfinden, welche Sorte der Aufrufer ist, und diese Angabe liegt in einer
Spalte irgendeiner Tabelle. Wie diese Tabelle auch heißt, die Regel, die sie
schützt, liest am Ende sie. In einer App aus Lovable, Bolt, v0 oder Replit ist
das meistens profiles, weil dort die Zeile über jede angemeldete Person
landet. Eine Regel über Teams, Organisationen oder geteilte Dokumente erzeugt
denselben Kreis auf der Tabelle, in der die Mitgliedschaft steht.
Die Rekursion entsteht also daraus, dass du die naheliegende Regel über die eine Tabelle geschrieben hast, die eine Frage über sich selbst beantworten muss.
Der Fix: ein Helfer, der die Tabelle außerhalb der Regeln liest
Verlege die Frage in eine kleine Funktion, die als Eigentümer der Datenbank
läuft, damit das Lesen von profiles für diese Antwort nicht durch die Regel
auf profiles geht.
create schema if not exists private;
create function private.is_admin()
returns boolean
language sql
security definer -- läuft als die Rolle, die sie erstellt hat
set search_path = '' -- damit jeder Name darin voll ausgeschrieben werden muss
stable
as $$
select exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'admin'
);
$$;
revoke execute on function private.is_admin() from public;
grant usage on schema private to authenticated;
grant execute on function private.is_admin() to authenticated;
-- Die Regel fragt jetzt die Funktion statt der Tabelle.
create policy "admins read every profile" on profiles for select
to authenticated
using ( (select private.is_admin()) );
Der Türsteher hat die Gästeliste jetzt auf einem Tisch im Flur. Er liest sie, ohne die Tür zu öffnen, und die Tür bleibt für alle zu, die nicht darauf stehen.
Vier Zeilen darin tun bestimmte Arbeit, und drei davon sind der Unterschied zwischen einem Fix und einem neuen Loch:
security definerist das, was den Kreis bricht. Supabases Leitfaden definiert es als eine Funktion, die unter derselben Rolle läuft, die die Funktion erstellt hat, und auf Supabase ist diese Rollepostgres, die an den Regeln vorbeilesen darf.set search_path = ''gehört an jede einzelne davon. Supabase rät, es auf jeder security definer Funktion zu setzen und die Tabellennamen voll auszuschreiben, denn ohne festgenageltensearch_pathkann ein Aufrufer einen unqualifizierten Namen auf ein eigenes Objekt zeigen lassen und ihn mit den Rechten des Funktionseigentümers laufen lassen. Deshalb schreibt die Funktionpublic.profilesvoll aus.- Das Schema
privateist aus demselben Grund wichtig. Supabases Leitfaden warnt, dass eine security definer Funktion in einem veröffentlichten Schema über die Data API mit den Rechten ihres Erstellers aufgerufen werden kann, und sagt dir, niemals eine in einem Schema anzulegen, das in deinen API-Einstellungen unter Exposed schemas steht. (select private.is_admin()), in ein eigenes select gewickelt. Dadurch rechnet Postgres die Antwort einmal für die ganze Anweisung aus statt einmal pro Zeile, was Supabase als Grund dokumentiert,auth.uid()genauso zu wickeln.
Die Zeilen revoke und grant halten die Funktion für angemeldete Nutzer
aufrufbar und für sonst niemanden.
Diese Policy deckt das Lesen ab. Wenn das Speichern zu scheitern beginnt, sobald deine Bildschirme sich wieder füllen, bist du einer anderen Meldung mit einer anderen Ursache begegnet.
Der Fix, der keiner ist
Row Level Security auf profiles abzuschalten beendet den Fehler ebenfalls, mit
einem Klick, und es gibt jede Zeile dieser Tabelle an jeden weiter, der den
Schlüssel hat, den deine App an den Browser ausliefert.
Dieser Schlüssel ist als öffentlich gedacht. Er steht im Code deiner Seite, und jeder kann ihn in ein paar Sekunden herauslesen. Was ihn bisher davon abgehalten hat, deine ganze Nutzertabelle zurückzugeben, waren die Regeln, die du gerade abgeschaltet hast.
Deine App sieht danach in beiden Fällen gleich aus, und genau das macht diese Sache so hartnäckig. Nichts auf deinen Bildschirmen sagt, welche der beiden Varianten du gewählt hast, und der Fehler ist in beiden weg.
Was es sagt, ist deine Datenbank, von außen gefragt. Wir haben 31.056 live geschaltete Apps aus Lovable, Bolt, v0, Replit und dem Rest gescannt und hinter 8.435 davon ein Supabase-Projekt gesehen. Das hat die Row-Level-Security-Prüfung gefunden:
| Was wir gefragt haben | Apps |
|---|---|
| Hatten ein sichtbares Supabase-Projekt | 8.435 |
| Konnten überhaupt gefragt werden | 3.680 |
| Gaben Zeilen an eine Anfrage ohne Login zurück | 2.096 |
| Davon aus einer Tabelle für Personen, Bestellungen, Nachrichten | 394 |
Die mittlere Zeile ist die ehrliche. Bei 4.755 dieser Apps bekam die Prüfung keine Antwort, und wir sagen das, statt sie für sauber zu erklären. Und einige der 2.096 sollen lesbar sein, denn eine öffentliche Tabelle ist etwas, das man wirklich wollen kann; von außen sehen eine Speisekarte und eine Mitgliederliste gleich aus.
Wir können dir nicht sagen, wie viele dieser Tabellen jemand beim Beheben genau dieses Fehlers geöffnet hat. Wir können dir sagen, dass eine offene Tabelle der gewöhnliche Endzustand des Rats ganz oben in den Suchergebnissen ist.
Wenn du lieber nicht darüber nachdenken willst: unser kostenloser Scan liest deine Live-Seite und sagt dir, welche deiner Tabellen einem Fremden antworten. Er dauert etwa 20 Sekunden und braucht kein Konto: scanne deine App.
Noch etwas passiert, wenn du es abschaltest. Supabases eigener Advisor fängt an, die Tabelle zu melden, das ist die Warnung RLS disabled in public, und diese Warnung steht in einem Monat immer noch da.
Wie du merkst, ob der Kreis wirklich weg ist
Dass der Fehler aufhört, ist nicht der Test, denn zwei verschiedene Dinge lassen ihn aufhören.
Es gibt außerdem einen Weg, auf dem die Hilfsfunktion richtig aussieht und
trotzdem weiter rekursiv bleibt, und Supabase dokumentiert ihn: eine security
definer Funktion geht nur dann an den Regeln vorbei, wenn ihr Eigentümer das
darf. Auf Supabase ist der Eigentümer postgres, der dieses Recht hat, also
funktioniert das Muster oben. Eine Funktion, die einer Rolle ohne dieses Recht
gehört, oder eine, die eine Tabelle mit force row level security liest, geht
wieder durch die Regel und bleibt im Kreis.
Zwei Prüfungen, und mach beide:
Schreib die Fälle auf und lass sie laufen. Supabases Verfahren ist eine
.sql-Datei unter supabase/tests/, die für jede Operation erlaubt und
verboten behauptet, für einen angemeldeten Nutzer und für einen anonymen,
ausgeführt mit supabase test db. Ein Admin muss jedes Profil sehen, ein
Mitglied sein eigenes, ein Fremder nichts. Bis das durchläuft, weißt du nur,
dass der Fehler aufgehört hat.
Dann frag von außen, ohne Login. Das ist die Frage, die der Browser eines Besuchers stellt, und die einzige, die abbildet, was deine App tatsächlich herausgibt. Ob ein Fremder deine Datenbank lesen kann geht das durch, und Row Level Security kann an sein und die Tabelle trotzdem öffentlich behandelt den Fall, in dem die Regeln da sind und trotzdem alle durchlassen.
Wenn die Rekursion sagt, dass das Schema falsch ist
Manchmal gibt es keine kreisförmige Frage zum Entfernen, weil die beiden Tabellen wirklich voneinander abhängen.
Supabases Leitfaden nimmt das Teilen als Beispiel: eine Regel auf lists prüft
list_members, um zu sehen, mit wem die Liste geteilt wurde, und eine Regel auf
list_members prüft lists, um zu sehen, wem sie gehört. Die Regel jeder
Tabelle liest die andere, und keine kann zuerst. Derselbe Helfer bricht das auf,
mit einer Funktion, die die Listen-Ids zurückgibt, zu denen der Aufrufer gehört,
und beiden Regeln, die diese Funktion fragen statt einander.
Das Signal, auf das es sich zu achten lohnt, ist, wenn du drei oder vier davon brauchst, damit ein Schema funktioniert. An dem Punkt wird die Mitgliedschaft jedes Mal innerhalb der Regeln neu gebaut, und eine einzelne Tabelle, die klar sagt, wer was sehen darf, ist meistens weniger Pflege als die Regeln, die du gerade entwirrst.
Die Tabelle zu halten, nachdem du sie repariert hast
Eine Regel, die du heute repariert hast, beschreibt die Datenbank von heute. Die nächste Policy, die nächste Tabelle und das nächste Mal, dass jemand um Mitternacht einen Fehler wegklickt, passieren alle, nachdem du zuletzt hingesehen hast, und keines davon ändert etwas, das du auf deinen Bildschirmen sehen kannst.
Reeve Monitor stellt deiner App dieselben Fragen noch einmal, nach Plan:
- alle neun Prüfungen jede Stunde, für bis zu drei Apps
- eine Nachricht, wenn sich ein Ergebnis ändert, damit eine Tabelle, die heute Nacht aufgegangen ist, nicht wartet, bis du es merkst
- ob die App erreichbar ist, alle 60 Sekunden
- ein monatlicher Bericht über das Gesehene
Wenn deine App ihre Daten in deinem eigenen Supabase-Projekt hält, hält Reeve Care zusätzlich eine Kopie dieser Datenbank. Eine Regel, die einen Fremden lesen lässt, ist das eine Problem. Eine Regel, die einen Fremden schreiben lässt, ist das andere, und keine Prüfung aus diesem Artikel holt gelöschte Zeilen zurück.
- eine verschlüsselte Kopie deiner Supabase-Datenbank jede Nacht, dort abgelegt, wo dein Projekt nicht hinkommt
- jede Kopie geprüft, bevor sie zählt, durch Zählen der Zeilen in jeder Tabelle
- eine Wiederherstellung per Klick, wenn du sie brauchst
- deine hochgeladenen Dateien ebenfalls, sobald du eine Storage-Zugangsberechtigung verbindest
- alles, was Monitor tut
Was du jetzt tun kannst
Was zu tun ist
- Lass Row Level Security an. Der Fehler betrifft eine Regel, und die Regeln abzuschalten ist eine Änderung an der ganzen Tabelle.
- Verlege die Rollenprüfung in eine
security definerFunktion in einem nicht veröffentlichten Schema, mitset search_path = ''und jedem Tabellennamen voll ausgeschrieben. - Zeige die Policy auf die Funktion, gewickelt als
(select private.is_admin()), damit sie einmal pro Anweisung läuft statt einmal pro Zeile. - Schreib die erlaubten und verbotenen Fälle unter
supabase/tests/und führesupabase test dbaus: ein Admin sieht jedes Profil, ein Mitglied sein eigenes, ein Fremder nichts. - Wenn du Row Level Security schon abgeschaltet hast, um weiterzukommen, schalte es heute zurück und repariere die Regel richtig. Supabases Advisor meldet diese Tabelle weiter, bis du das tust, und es gehört auf jede Tabelle, die du hast.
Wenn du den Rest der Liste für eine frisch gestartete App willst: die 10-Minuten-Sicherheitscheckliste deckt das und die anderen Dinge ab, die zu schließen sich lohnt, bevor sie jemand findet.
FAQ
Was verursacht "infinite recursion detected in policy for relation"?
Eine Regel auf einer Tabelle, die genau diese Tabelle lesen muss, bevor sie antworten kann. Deine Regel sagt sinngemäß "lass diese Person durch, wenn ihre Zeile in profiles sie als Admin ausweist", also geht die Datenbank diese Zeile in profiles lesen, wofür sie die Regel auf profiles prüfen muss, was sie wieder zum Lesen derselben Zeile schickt. Postgres merkt, dass es im Kreis läuft, hört auf und meldet den Fehler 42P17. Dasselbe passiert über zwei Tabellen hinweg, deren Regeln jeweils die andere lesen.
Ist eine security definer Funktion in einer Policy sicher?
Ja, unter zwei Bedingungen, die Supabase in seinem eigenen Row-Level-Security-Leitfaden nennt. Setze search_path auf den leeren String und schreibe jeden Tabellennamen darin voll aus, also public.profiles statt profiles. Ohne das kann jemand einen unqualifizierten Namen auf ein eigenes Objekt zeigen lassen und es mit den Rechten des Funktionseigentümers laufen lassen. Und lege die Funktion in einem Schema an, das nicht über die API veröffentlicht ist, denn eine security definer Funktion in einem veröffentlichten Schema lässt sich von außen mit den Rechten ihres Erstellers aufrufen.
Soll ich RLS abschalten, um das zu beheben?
Das beendet den Fehler und öffnet die Tabelle. Mit abgeschaltetem Row Level Security kann jede Zeile darin von jedem gelesen werden, der den Schlüssel hat, den deine App an den Browser ausliefert, und diesen Schlüssel kann jeder aus deiner Seite herauslesen. Deine App verhält sich in beiden Fällen gleich, also sagt dir danach nichts mehr, welche der beiden Varianten du gewählt hast. Die Hilfsfunktion weiter unten beendet den Fehler und hält die Tabelle zu.
Warum passiert das immer an der profiles-Tabelle?
Weil in profiles üblicherweise die Antwort auf "wer ist diese Person" steht. Eine Regel, die Admins anders behandelt als andere, oder Mitglieder eines Teams anders, muss wissen, was der Aufrufer ist, und diese Angabe liegt in profiles. Also liest die Regel, die profiles schützt, am Ende profiles. Jede Tabelle, die Rolle oder Mitgliedschaft des Aufrufers hält, kann das auslösen, und in einer App aus Lovable, Bolt oder v0 ist das meistens die Tabelle namens profiles.
Wie prüfe ich, ob meine Policy wirklich funktioniert?
Zwei verschiedene Dinge beenden den Fehler, also sagt das Verschwinden des Fehlers für sich genommen wenig. Schreibe die erlaubten und die verbotenen Fälle in eine .sql-Datei unter supabase/tests/ und führe supabase test db aus, das ist das Verfahren, das Supabase mit dem Leitfaden veröffentlicht: ein angemeldeter Eigentümer muss durchkommen und ein Fremder muss abgewiesen werden. Stelle dann dieselbe Frage von außen an deine Datenbank, ganz ohne Login, so wie es der Browser eines Besuchers tut.