Zum Inhalt springen

Sicherheitsgrundlagen

Row Level Security für jede Supabase-Tabelle aktivieren und prüfen

Row Level Security in Supabase ohne Policy sperrt eine Tabelle komplett. Eine Policy ohne den Schalter bringt gar nichts. Hier ist das SQL, und der Test.

Vlad Tkachenko12 Min. Lesezeit
Eine Liste von Datenbanktabellen in einem Panel, neben jeder ein Sicherheitsschalter, und die meisten davon stehen noch auf aus.

Kurz gesagt

  • Row Level Security in Supabase ohne Policy sperrt eine Tabelle komplett, und eine Policy zu schreiben, ohne den Schalter anzumachen, bringt gar nichts. Jede Tabelle braucht beides.
  • Drei Policy-Formen decken fast alles ab, was ein KI-Baukasten anlegt: Zeilen, die einer Person gehören, Zeilen, die jeder lesen darf, und Zeilen, die deine App für einen Besucher schreibt.
  • Prüf danach von außerhalb deiner App ohne Anmeldung, denn das ist die Anfrage, die ein Fremder stellt, und nur sie sagt dir, was deine Policies tun statt was sie behaupten.

Man hat dir gesagt, du sollst Row Level Security anschalten, oder dein KI-Baukasten hat es beiläufig erwähnt, während er etwas anderes repariert hat. In deinem Supabase-Projekt liegen irgendwo zwischen vier und vierzig Tabellen, und du weißt nicht, welche davon abgedeckt sind.

Die Anleitung, die du überall findest, ist eine Zeile SQL pro Tabelle, und so weit stimmt sie auch. Wovor fast jede Anleitung haltmacht, ist der Schritt danach. Eine Policy, die existiert, ist keine Policy, die funktioniert, und nichts in deinem Dashboard zeigt dir den Unterschied. Row Level Security in Supabase zu aktivieren sind also drei Arbeiten statt einer: den Schalter über alle Tabellen legen, die zwei oder drei Policies schreiben, die deine App wieder zusammensetzen, und dann deine eigene Datenbank das fragen, was ein Fremder sie fragen würde.

Was macht Row Level Security eigentlich?

Es sorgt dafür, dass Postgres deine Regeln prüft, bevor es eine Zeile herausgibt. Ist der Schalter aus, gibt es keine Regeln zu prüfen, und deshalb lautet die Antwort auf jede Anfrage: alles.

Stell dir einen Bibliothekar vor, der holt, was immer du verlangst. Row Level Security ist die Anweisung, vor dem Beladen des Wagens einen Zettel über dich zu lesen. Der Zettel ist deine Policy, und darauf kann stehen, dass diese Person die Bücher mitnehmen darf, die sie selbst geschrieben hat, oder dass jeder alles mitnehmen darf. Gilt die Anweisung und ist noch kein Zettel geschrieben, kommt der Bibliothekar mit einem leeren Wagen zurück und sagt nicht, warum.

Genau daran stolpern die Leute, und es entscheidet darüber, wie ein Test später aussieht. Row Level Security filtert Zeilen. Sie weist keine Anfragen ab. Eine Tabelle, die du nicht lesen darfst, antwortet mit einer leeren Liste und einem Erfolgscode, nicht mit einem Fehler oder einer Anmeldeaufforderung. Deiner App wird nie gesagt, dass sie abgewiesen wurde. Sie bekommt nichts und zeigt einen leeren Bildschirm.

Eine Anfrage, drei Einstellungen und jedes Mal derselbe Erfolgscode. Row Level Security ändert, was zurückkommt, nie ob die Anfrage geklappt hat.

Den Schalter über ein Projekt zu legen, in dem noch keine Policies stehen, sperrt also keine Fremden aus deinen Daten aus. Es sperrt alle aus, deine eigene App eingeschlossen, bis du sagst, wer was sehen darf.

Wie aktiviere ich RLS in jeder Supabase-Tabelle auf einmal?

Eine Schleife, einmal im SQL Editor ausgeführt. Sie geht jede Tabelle in deinem public-Schema durch und legt den Schalter überall dort um, wo er aus ist.

Sieh dir zuerst an, wo du stehst. Das hier listet deine Tabellen auf und sagt zu jeder, ob sie ihn gerade hat:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;

Jede Zeile, in der rowsecurity auf false steht, ist eine Tabelle, die ihren Inhalt an jeden herausgibt, der den veröffentlichbaren Schlüssel hat, den deine App ausliefert. Ist die Liste kurz, mach sie einzeln und sieh zu, was kaputt geht:

alter table public.orders enable row level security;

Ist sie nicht kurz, erledigt das hier alles:

do $$
declare t record;
begin
  for t in
    select tablename from pg_tables where schemaname = 'public'
  loop
    execute format(
      'alter table public.%I enable row level security', t.tablename
    );
  end loop;
end $$;

Führ das aus, und deine App wird leer. Supabase schreibt es in der eigenen Dokumentation klar hin: Über die API mit einem veröffentlichbaren Schlüssel sind die Daten nicht mehr erreichbar, bis Policies definiert sind. Das ist der Schalter bei der Arbeit, und deshalb ist der nächste Abschnitt der, den du offen haben solltest, bevor du auf Ausführen drückst.

Die drei Policies, die du wirklich brauchst

Fast jede Tabelle in einer App wie deiner hat eine von drei Formen: Zeilen, die einer Person gehören, Zeilen, die jeder lesen darf, und Zeilen, die deine App für einen Besucher schreibt. Hier ist jede davon, fertig zum Einfügen und Umbenennen.

Zeilen, die einer Person gehören. Bestellungen, Nachrichten, gemerkte Artikel, alles mit einem Besitzer.

create policy "read own orders"
on public.orders for select
to authenticated
using ( (select auth.uid()) = user_id );

auth.uid() ist die id dessen, der bei dieser Anfrage angemeldet ist. user_id ist die Spalte deiner Tabelle, in der der Besitzer steht, also prüf den Namen, bevor du das ausführst: Baukästen schreiben auch owner_id, profile_id und created_by. Die Zeile to authenticated bedeutet, dass die Policy für einen abgemeldeten Besucher gar nicht erst betrachtet wird, und genau das hält die Tabelle vor der Öffentlichkeit zu.

Zeilen, die jeder lesen darf. Ein Produktkatalog, veröffentlichte Artikel, eine Karte mit Veranstaltungsorten.

create policy "anyone may read products"
on public.products for select
to anon, authenticated
using ( true );

Schreib die hier bewusst oder gar nicht, denn es ist auch die Policy, zu der ein KI-Baukasten greift, sobald du ihn bittest, einen leeren Bildschirm zu reparieren. Die Frage, die vorher zu klären ist: Könnte diese Tabelle genau so, wie sie dasteht, eine Seite auf deiner Website sein, ohne dass etwas herausgenommen wird? Ein Nein heißt, sie will stattdessen die Besitzer-Policy von oben.

Zeilen, die deine App schreibt. Lesen und Schreiben sind in Postgres getrennte Rechte, also braucht eine Tabelle, in die deine App speichert, eine zweite Policy, und die prüft die Zeile auf dem Weg hinein statt auf dem Weg hinaus.

create policy "insert own orders"
on public.orders for insert
to authenticated
with check ( (select auth.uid()) = user_id );

Welche Klausel wohin gehört, ist der Teil, an dem es hakt, und die Zeile update trägt eine Anforderung, an der nichts naheliegend ist:

OperationKlauselBraucht außerdem
selectusing
insertwith check
updateusing und with checkeine select-Policy auf derselben Tabelle
deleteusing

Supabases Dokumentation ist bei der letzten deutlich: Ohne passende select-Policy funktioniert ein update nicht wie erwartet. Und wenn dich die Meldung new row violates row-level security policy überhaupt erst hierher geführt hat, dann ist der Abstand zwischen diesen beiden Klauseln der ganze Fehler.

Warum jedes Beispiel (select auth.uid()) statt auth.uid() schreibt

Weil die Klammern Postgres den Wert einmal für die ganze Abfrage ausrechnen lassen statt einmal für jede Zeile, die es ansieht.

Die eingepackte Fassung wird zu dem, was Postgres einen initPlan nennt: einmal ausgeführt und danach für den Rest der Anweisung wiederverwendet. Ohne die Klammern wird die Funktion bei Zeile eins wieder aufgerufen, bei Zeile zwei, bei Zeile drei, und so weiter durch eine Tabelle, in der hunderttausend davon liegen können. Supabases eigener Performance Advisor meldet die Fassung ohne Klammern unter einer Regel namens Auth RLS Initialization Plan, und sie gehört zu den Einträgen, die dort am häufigsten stehen.

Die Klammern sind der ganze Unterschied. Links läuft die Funktion einmal pro Zeile, rechts läuft sie einmal und die Antwort wird wiederverwendet.

Ein Vorbehalt, und der kommt von Supabase selbst: Das funktioniert, weil die Antwort von Zeile zu Zeile gleich bleibt. Eine Funktion, deren Ergebnis tatsächlich von der Zeile vor ihr abhängt, lässt sich nicht aus der Schleife ziehen, die also ohne Klammern lassen.

Wenn du schon dabei bist, leg einen Index auf die Spalte, nach der deine Policies filtern. Die Policy wird zu einer Bedingung bei jedem Lesen dieser Tabelle, und bei einer Tabelle mit vielen Zeilen macht sich eine Spalte ohne Index in deinen Antwortzeiten bemerkbar:

create index orders_user_id_idx on public.orders (user_id);

Eine Policy testen, ohne ein falsches Konto anzulegen

Der Supabase SQL Editor kann eine Abfrage so ausführen, als hätte ein bestimmter Besucher sie geschickt, und damit ist der anonyme Fall komplett abgedeckt.

Der Editor hat ein Bedienelement für die Rolle, unter der eine Abfrage laufen soll. Stell es auf die anonyme Rolle und führ dann ein gewöhnliches select gegen die Tabelle aus, die du gerade geändert hast. Was zurückkommt, ist das, was ein abgemeldeter Fremder bekommt. Für einen angemeldeten Besucher nimm die id eines Nutzers, den du schon hast, statt einen neuen anzulegen; jede Zeile in deiner eigenen Tabelle reicht zum Testen.

Wenn du lieber tippst als klickst, dasselbe in SQL:

begin;
set local role anon;
select * from public.orders;
rollback;

begin und rollback stehen da, damit der Rollenwechsel nur für diesen Block gilt und nicht länger, was zählt, wenn du in einem Rutsch mehrere Tabellen durchgehst.

Was dir das sagt, ist, was deine Policies innerhalb der Datenbank tun. Was es dir nicht sagen kann, ist, was dein Projekt ins Internet hinausgibt, denn die Anfrage, die deine Besucher tatsächlich stellen, beginnt nicht im SQL Editor. Sie beginnt in einem Browser, trägt einen veröffentlichbaren Schlüssel und kommt über die öffentliche Adresse deines Projekts an.

Wie teste ich Supabase RLS von außerhalb meiner App?

Schick die Anfrage, die ein Fremder schicken würde. Du brauchst zwei Werte, und beide stecken schon in dem Code, den deine Seite jedem Besucher ausliefert: die URL deines Projekts und deinen veröffentlichbaren Schlüssel.

curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
  -H "apikey: YOUR_PUBLISHABLE_KEY" \
  -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY"

Setz einen Tabellennamen nach dem anderen ein und lies, was zurückkommt. Eine leere Liste, [], heißt, die Policy hat für einen abgemeldeten Besucher gehalten. Eine Zeile heißt, dass jeder mit einem Schlüssel, den deine App ausliefert, diese Tabelle lesen kann. Ein Fehler, der den Schlüssel selbst erwähnt, heißt, du hast den falschen Wert kopiert, und das schließt man besser aus, bevor man irgendetwas folgert.

Der Test im Editor verlässt das Gebäude nie. Die Anfrage von außen kommt über dieselbe öffentliche Adresse an, die deine Besucher benutzen, und trägt denselben Schlüssel, den deine App ausliefert.

Bei einem neueren Supabase-Projekt beginnt dieser Schlüssel mit sb_publishable_, bei einem älteren ist es der anon-Schlüssel. Beide sind so sicher zu benutzen und sicher in deiner App, das ist ihr Zweck; welche API-Schlüssel in ein Frontend gehören behandelt das Paar, das dort nicht hingehört, und wo du sie im Dashboard findest sind die vier Werte auf dieser Einstellungsseite.

Das ist der Test, der der Wirklichkeit entspricht, und ihn in großem Maßstab zu fahren ist der Grund, warum wir wissen, wie verbreitet die Lücke ist. Zwischen dem 12. und 14. August 2026 haben wir neun externe Prüfungen über 30.998 laufende Apps aus Lovable, Base44, Replit, v0 und Bolt fahren lassen. Von den 3.680 Supabase-Apps, bei denen die Prüfung durchlief, haben 2.096 auf eine anonyme Anfrage Zeilen aus mindestens einer Tabelle zurückgegeben. Das sind 57 %, und zwar als Anteil an den Apps, die uns eine klare Antwort gegeben haben, nicht an allem, was wir gescannt haben. Der vollständige Datensatz ist veröffentlicht, und worauf sich diese 57 % beziehen geht die Zählung durch.

Von Hand ist das bei vier Tabellen in Ordnung und bei vierzig mühsam. Unser kostenloser Scan schickt diese Anfrage für dich, findet heraus, welche Tabellen es gibt, ohne dass du sie nennst, und sagt dir, welche geantwortet haben, neben acht weiteren Prüfungen, die er von außen macht. Er liest deine Live-Seite so, wie jeder Besucher es kann, dauert etwa 20 Sekunden und braucht kein Konto: App scannen.

Bevor du Policies an einer Live-Tabelle umschreibst

Nimm zuerst eine Kopie deiner Datenbank. Du bist dabei, Rechte über jede Tabelle zu ändern, die du hast, mit denselben Werkzeugen, die das Problem erzeugt haben.

Zwei verschiedene Risiken lohnt es sich zu trennen, denn nur eines davon betrifft die Arbeit von heute. War eine Tabelle für jeden les- und schreibbar, dann ändert es nichts an dem, was schon passiert ist, sie jetzt zuzuziehen, und diese Variante zeigt sich meist als Support-Nachricht über Daten, die sich von selbst geändert haben. Das andere Risiko ist die Migration selbst. Eine Policy, die gelöscht und leicht falsch wieder angelegt wird, ist ein ganz gewöhnlicher Dienstag, und der Weg zurück ist eine Kopie davon, wie es vor einer Stunde aussah.

Auf einem bezahlten Supabase-Plan wartet die Kopie von gestern Nacht in der Konsole. Auf dem kostenlosen Plan gibt es gar nichts, worauf man zurückfallen könnte, weil Supabase im kostenlosen Tarif keine automatischen Backups macht. Wenn das auf dich zutrifft, nimm eines, bevor du anfängst.

Reeve Care ist die Fassung davon, an die du nicht denken musst. Deine Supabase-Datenbank wird nach Zeitplan kopiert, außerhalb deines Supabase-Kontos aufbewahrt, verschlüsselt und zurückgelesen, um zu bestätigen, dass sie sich wiederherstellen lässt, bevor sich das Datum in deinem Dashboard bewegt. Hochgeladene Dateien reisen mit, sobald du einen Storage-Schlüssel verbindest, und dieser Schlüssel wird getrennt abgefragt, weil Supabase für Dateien keinen Nur-Lese-Schlüssel ausgibt: Der, der deine Uploads kopiert, kann auch schreiben, der, der deine Datenbank kopiert, nicht. Ihn zu verbinden ist freiwillig, und deine Datenbank wird so oder so gesichert. Backups gibt es nur für Supabase, und wenn deine Daten woanders liegen, sagen wir das, statt dir ein Abo für eine leere Kiste zu verkaufen.

Das Wiederherstellen ist der Teil, auf den es an einem Tag wie diesem ankommt. Care kopiert den aktuellen Stand deiner Datenbank, bevor es die Fassung einspielt, die du ausgewählt hast, das Drücken des Knopfes hat also selbst ein Rückgängig.

Die andere Hälfte ist die Prüfung, die du gerade von Hand gemacht hast. Eine Policy, die sich bei irgendeiner späteren Migration lockert, findet niemand durch Hinsehen, also fährt Care dieselbe anonyme Anfrage nach Zeitplan erneut und sagt dir, wenn eine Tabelle zu antworten beginnt, die letzte Woche still war. Uptime-Überwachung und ein Monatsbericht liegen im selben Abo. Nichts davon schreibt deine Policies für dich, und kein Backup macht eine offene Tabelle zu. Was sich ändert, ist, was eine schiefgegangene Migration dich kostet. Was Reeve bei Supabase sichert, wie oft, und was eine Wiederherstellung tut geht den ganzen Kreis durch, und die Tarife stehen auf der Preisseite.

Die Reihenfolge, in der du arbeitest

Was zu tun ist

  • Lass dir mit select tablename, rowsecurity from pg_tables where schemaname = 'public' deine Tabellen anzeigen und sieh, wie viele offen sind, bevor du etwas änderst.
  • Nimm ein Backup und schalte dann Row Level Security in jeder Tabelle im public-Schema an. Rechne damit, dass deine App leer wird; das ist der Schalter bei der Arbeit.
  • Gib jeder Tabelle eine der drei Policies. Besitz ist der Normalfall; USING (true) ist eine Entscheidung, die du Tabelle für Tabelle triffst, und kein Weg, die Bildschirme zurückzubekommen.
  • Schreib (select auth.uid()) statt auth.uid() und leg einen Index auf die Spalte, nach der deine Policies filtern.
  • Prüf jede Tabelle von außen ohne Anmeldung. Eine leere Liste ist bestanden. Eine Zeile ist eine Tabelle, die jeder lesen kann, was auch immer dein Dashboard dazu sagt.

Fang mit der Tabelle an, die dir als öffentliche Seite am peinlichsten wäre, und arbeite dich von dort nach unten. Die 10-Minuten-Sicherheitscheckliste deckt das zusammen mit dem Rest ab, was in einer frisch gestarteten App zu bestätigen ist, und der Supabase-Sicherheitsleitfaden geht durch, was sonst noch offen zu bleiben pflegt.

FAQ

Was passiert, wenn ich RLS anschalte und keine Policy schreibe?

Die Tabelle hört auf zu antworten, auch für deine eigene App. Ist der Schalter an, prüft Postgres deine Policies, bevor es eine Zeile herausgibt, und ohne Policies gibt es nichts, was ja sagen könnte, also kommen Anfragen leer zurück. Supabase schreibt das direkt hin: Über die API mit einem veröffentlichbaren Schlüssel sind die Daten nicht mehr erreichbar, bis Policies definiert sind. Es wird nichts gelöscht und nichts geht kaputt. Schreib eine Policy für die Zeilen, die deine App zeigen soll, und die Bildschirme kommen zurück.

Macht Row Level Security meine Abfragen langsamer?

Das kann sein, und die beiden Abhilfen sind klein. Schreib `(select auth.uid())` in die Policy-Bedingung, mit den Klammern, damit Postgres den Wert einmal für die ganze Abfrage ausrechnet und ihn danach für jede Zeile wiederverwendet. Supabase meldet die Fassung ohne Klammern im eigenen Performance Advisor, unter einer Regel namens Auth RLS Initialization Plan. Leg dann einen Index auf die Spalte, nach der die Policy filtert, meist `user_id`. Bei einer Tabelle mit ein paar tausend Zeilen wirst du so oder so kaum etwas merken. Bei einer großen zählt beides.

Brauche ich RLS, wenn in meiner Tabelle keine persönlichen Daten stehen?

Angeschaltet haben willst du es trotzdem, und die Policy darf die freizügige sein. Eine Tabelle mit ausgeschalteter Row Level Security kann jeder lesen, der den veröffentlichbaren Schlüssel hat, den deine App ausliefert, und das ist für einen Produktkatalog in Ordnung und für alles deutlich weniger in Ordnung, was du nicht als Seite veröffentlichen würdest. Sie anzuschalten und eine Lese-Policy mit `USING (true)` zu schreiben gibt dir denselben öffentlichen Zugang mit Absicht, und die Tabelle liest sich beim nächsten Durchgehen der Liste als entschieden statt als vergessen.

Warum funktioniert mein Realtime-Abo nicht mehr, seit ich RLS angeschaltet habe?

Weil Realtime dieselben Policies prüft, bevor es eine Änderung an einen Abonnenten schickt. Eine Tabelle mit angeschalteter Row Level Security und ohne select-Policy für diesen Besucher liefert einer Abfrage keine Zeilen und einem Abo keine Änderungen, aus genau demselben Grund. Füg die select-Policy hinzu, die dieser Abonnent braucht, und der Strom läuft weiter. Wenn du Realtime Broadcast oder Presence statt Datenbankänderungen nutzt, werden die getrennt autorisiert, über Policies auf der Tabelle `realtime.messages`.

Wie teste ich eine Policy, ohne einen falschen Nutzer anzulegen?

Auf zwei Wegen, und keiner davon braucht ein neues Konto. Der Supabase SQL Editor kann eine Abfrage so ausführen, als hätte eine bestimmte Rolle sie geschickt, und damit ist der anonyme Fall komplett abgedeckt: Was zurückkommt, ist das, was ein Fremder bekommt. Für den angemeldeten Fall brauchst du eine Nutzer-id als Platzhalter, und jede Zeile, die schon in deiner Tabelle steht, tut es. Der zweite Weg ist eine Anfrage von außen mit deinem veröffentlichbaren Schlüssel, und die prüft den ganzen Pfad statt nur die Policy.

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.