Zum Inhalt springen

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.

Vlad Tkachenko10 Min. Lesezeit
Eine verschlossene Tabellenfläche, deren eigener Schlüssel hinter dem Glas liegt, mit einem Riegel quer über der Tür.

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.

Mit der Anfrage ist alles in Ordnung. Der Kreis liegt zwischen der Regel und der Tabelle, die die Regel schützt.

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 definer ist 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 Rolle postgres, 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 festgenagelten search_path kann ein Aufrufer einen unqualifizierten Namen auf ein eigenes Objekt zeigen lassen und ihn mit den Rechten des Funktionseigentümers laufen lassen. Deshalb schreibt die Funktion public.profiles voll aus.
  • Das Schema private ist 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.

Beide beenden den Fehler. Nur einer von beiden beantwortet die Frage noch, die der Regel gestellt wurde.

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 habenApps
Hatten ein sichtbares Supabase-Projekt8.435
Konnten überhaupt gefragt werden3.680
Gaben Zeilen an eine Anfrage ohne Login zurück2.096
Davon aus einer Tabelle für Personen, Bestellungen, Nachrichten394

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 definer Funktion in einem nicht veröffentlichten Schema, mit set 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ühre supabase test db aus: 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.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Unsicher, wie es um deine eigene App steht?

Starte einen kostenlosen Scan und erhalte in rund 20 Sekunden eine verständliche Note von A bis F. Ohne Konto, ohne Karte.

App kostenlos scannen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.