Zum Inhalt springen

Sicherheitsgrundlagen

Supabase Row Level Security ist an. Die Tabelle ist trotzdem offen.

Supabase Row Level Security anzuschalten schützt keine Tabelle. Erst deine Policies tun das, und die, die deine App repariert hat, lässt oft alle herein.

Vlad Tkachenko7 Min. Lesezeit

Kurz gesagt

  • In Supabase sind der Schalter und die Regeln zwei getrennte Dinge. An ohne Regel sperrt alle aus, an mit der falschen Regel sperrt niemanden aus.
  • Die Regel, die eine kaputte App wieder zum Laufen bringt, erlaubt meistens jede Anfrage von jedem.
  • Lies die Policy, nicht den Schalter. Ein einziges Wort darin entscheidet, ob Fremde deine Tabelle lesen können.

Du hast Row Level Security angeschaltet, weil irgendetwas dir dazu geraten hat: der Advisor in Supabase, eine Checkliste, ein Scan-Ergebnis, jemand in einem Discord. Der Schalter ist jetzt grün in deinem Dashboard. Und trotzdem hörst du, deine Tabelle sei weiterhin für Fremde lesbar.

Hier ist der Punkt, den Anleitung um Anleitung falsch darstellt: Der Schalter schützt gar nichts. Er entscheidet nur, dass deine Regeln überhaupt geprüft werden. Die Regeln sind eine eigene Sache, du musst sie selbst schreiben, und die Regel, die am schnellsten geschrieben ist (die, mit der eine kaputte App wieder läuft), lässt alle herein.

Row Level Security ist in Supabase an. Wieso ist die Tabelle noch offen?

Weil Anschalten und Festlegen, wer hereindarf, zwei verschiedene Schritte sind und nur der erste ein Schalter ist.

Stell dir den Schalter als jemanden vor, der an der Tür steht. Ihn umzulegen entscheidet nicht, wer hindurchdarf. Es entscheidet, dass jetzt jemand eine Liste abgleicht. Deine Policies sind diese Liste. Eine leere Liste weist alle ab, eine Liste mit „alle" weist niemanden ab. Beides ist Row Level Security im angeschalteten Zustand, und dein Dashboard zeigt in beiden Fällen dasselbe Grün.

Deshalb beantwortet die Einstellung für sich genommen sehr wenig, und deshalb fragt unser Scanner Supabase nie, ob sie aktiviert ist. Er fragt stattdessen die Tabelle. Er schickt die Anfrage, die auch ein Fremder schicken würde, mit dem öffentlichen Schlüssel aus deiner App, und sieht nach, ob eine Antwort zurückkommt. Er fragt nach einer Anzahl statt nach den Zeilen und erfährt so, dass die Tür aufging, ohne zu lesen, was dahinterliegt.

Die Policy, die deine App repariert hat, ist wahrscheinlich das Problem

In dem Moment, in dem du Row Level Security anschaltest, zeigt deine App keine Daten mehr, und was du danach getan hast, um sie wieder zum Laufen zu bringen, ist genau die Stelle, auf die es sich zu schauen lohnt.

Dieser Ablauf ist völlig normal, und hier geht es schief. Mit der Einstellung an und ohne geschriebene Policies weist Postgres (die Datenbank-Engine, auf der Supabase läuft) standardmäßig jede Anfrage ab, deine Listen kommen leer zurück und deine Bildschirme bleiben blank. Irgendetwas muss auf die Liste. Wenn du Cursor oder Lovable gebeten hast, das zu reparieren, oder den ersten Schnipsel eingefügt hast, der die Fehlermeldung beendet hat, sieht das Ergebnis vermutlich so aus:

CREATE POLICY "Enable read access for all users"
  ON public.profiles
  FOR SELECT
  USING (true);

USING (true) ist die Bedingung, die eine Zeile erfüllen muss, bevor die Datenbank sie herausgibt. Jede Zeile erfüllt true. Darin steckt ein zweites Detail, an dem man leicht vorbeigeht: Ohne TO-Klausel gilt eine Policy für public, und public umfasst angemeldete Besucher und völlig Fremde gleichermaßen.

Die App läuft also wieder, nichts zeigt einen Fehler, und die Tabelle ist genau so lesbar wie vorher.

Das repariert das Lesen, und nur das Lesen. Wenn deine App in diese Tabelle auch speichert, ist das Nächste, dem du begegnest, new row violates row-level security policy, also dieselbe Einstellung, die einen Schreibvorgang ablehnt, und keine Lese-Policy räumt das weg.

Die vier Zustände, in denen eine Tabelle sein kann

Zwei davon sind sicher und zwei nicht, und der Schalter sagt dir nicht, welche welche sind. „Öffentlicher Schlüssel" unten meint den, der in deine App gehört: sb_publishable_… in neueren Supabase-Projekten, anon in älteren.

Row Level SecurityDie PolicyDeine AppEin Fremder mit deinem öffentlichen Supabase-Schlüssel
Ausegal, keine wird geprüftLäuftLiest jede Zeile
Ankeine geschriebenKaputtLiest nichts
AnUSING (true)LäuftLiest jede Zeile
AnUSING (auth.uid() = user_id)LäuftLiest nur die eigenen
Das Häkchen unter jeder Spalte folgt nicht dem Schalter darüber. Zwei davon sind angeschaltet, und eines davon gibt alles heraus.

Die beiden mittleren Spalten sind das Paar, an dem Menschen hängen bleiben. Dieselbe Einstellung, dasselbe grüne Abzeichen, entgegengesetzte Ergebnisse, und der Unterschied ist ein Wort in einer Regel, die kaum jemand je öffnet.

Wenn du nicht jede Policy selbst lesen möchtest: Unser kostenloser Scan stellt deiner Datenbank live dieselbe Frage wie ein Fremder und sagt dir, welche Tabellen geantwortet haben. Er dauert rund 20 Sekunden und braucht kein Konto: App scannen.

„Authenticated" bedeutet nicht „dir gehörend"

Eine Policy, die authenticated erlaubt, erlaubt alle, die ein Konto haben, und wenn deine App ein offenes Anmeldeformular hat, sind das alle, die bereit sind, es auszufüllen.

Das ist die feinere Variante desselben Fehlers, und sie übersteht so manche Durchsicht, weil sie sorgfältig aussieht. TO authenticated USING (true) liest sich wie eine Einschränkung, und das ist es auch: Es schließt aus, wer sich nie angemeldet hat. Was es nicht tut, ist einen deiner Kunden davon abzuhalten, die Zeilen eines anderen Kunden zu lesen, und genau das meintest du vermutlich mit „privat".

Die Regel, die das leistet, benennt, wem die Zeile gehört:

CREATE POLICY "Users read their own rows"
  ON public.orders
  FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

auth.uid() ist, wer gerade fragt. user_id ist die Spalte auf der Zeile, die sagt, wem sie gehört. Die Zeile kommt zurück, wenn beides übereinstimmt, und bleibt liegen, wenn nicht.

So prüfst du deine eigenen Tabellen in zwei Minuten

Öffne Supabase, geh auf Authentication → Policies und lies den Ausdruck in jeder Policy statt das Abzeichen neben jeder Tabelle.

Drei Dinge, auf die du achten solltest:

  • Eine Policy, deren Bedingung true ist. Entscheide Tabelle für Tabelle, ob es dir recht wäre, diese Daten auf einer öffentlichen Seite stehen zu haben. Bei einer Liste veröffentlichter Artikel: ja. Bei allem, in dem ein Mensch vorkommt: nein.
  • Eine Policy ohne TO-Klausel. Sie gilt für alle, angemeldet oder nicht, auch wenn der Rest der Regel sehr konkret aussieht.
  • Eine Tabelle mit angeschalteter Einstellung, ganz ohne Policies, und einer App, die trotzdem läuft. Diese Kombination heißt, dass etwas anderes an deine Daten kommt, und die übliche Erklärung ist ein geheimer Schlüssel (sb_secret_…, oder service_role in einem älteren Projekt), der jede von dir geschriebene Policy ignoriert. Welche API-Schlüssel im Frontend sicher sind zeigt, wie du ihn vom sicheren unterscheidest.

Die andere Prüfung, zu der viele greifen, ist, die App im privaten Fenster ohne Anmeldung zu öffnen. Sie lohnt sich, aber wisse, was sie beweist. Deine App entscheidet, was sie anzeigt. Deine Datenbank entscheidet, was sie herausgibt. Das sind zwei verschiedene Entscheidungen, und ein Fremder, der deine Bildschirme überspringt, bekommt die zweite.

Die Bildschirme deiner App sind nicht der Zaun. Dieselbe Tabelle kann über deine App zwei Zeilen zeigen und einer Anfrage, die sie nie geöffnet hat, alle sechs herausgeben.

Wie es aussieht, wenn dir das gerade passiert

Nach nichts. Das ist die Form dieser Sache, und deshalb bleibt sie monatelang liegen.

Es erscheint kein Fehler in deinem Builder. Nichts wird langsamer, kein Bildschirm bricht, keine E-Mail kommt. Deine App verhält sich exakt wie am Tag des Starts, denn von ihrer Seite hat sich nichts geändert: Sie durfte diese Zeilen immer lesen. Geändert hat sich, dass alle anderen sie ebenfalls lesen dürfen, mit dem Schlüssel, der in dem Code steckt, den deine Seite an jeden Besucher ausliefert.

Wenn es auffällt, dann seitlich. Eine Kundin fragt, woher jemand etwas wusste, das nur deine App wusste. Eine Adressliste, die du nie veröffentlicht hast, taucht irgendwo auf. Und wenn die großzügige Regel auch das Schreiben abdeckt (FOR ALL statt FOR SELECT), dann können Zeilen auch von allen geändert und gelöscht werden. Diese Variante entdecken Leute als eine Tabelle, die plötzlich leer ist.

Regeln verrutschen außerdem. Eine Migration, eine Schemaänderung, noch eine nächtliche Reparatur, die Daten laden musste: Jede davon kann eine Policy lockern, ohne es zu sagen. Deshalb lohnt sich hier ein zweiter Blick später und nicht nur einer jetzt. Darauf zu achten ist Teil dessen, was Reeve Care tut. Eine Erinnerung im Kalender leistet dasselbe.

Was du diese Woche tun solltest

Was zu tun ist

  • Öffne in Supabase Authentication → Policies und lies die Bedingung jeder Policy, Tabelle für Tabelle. Das Abzeichen an der Tabelle ist nicht die Antwort.
  • Prüfe bei jeder Tabelle mit Menschen darin (Nutzer, Profile, Bestellungen, Nachrichten), ob die Regel den Besitzer der Zeile benennt, statt alle zu erlauben.
  • Ersetze jedes USING (true) auf diesen Tabellen durch eine Regel, die auth.uid() mit der Besitzerspalte vergleicht, und prüfe danach, ob deine App noch lädt.
  • Ergänze die TO-Klausel, die du gemeint hast. Eine Policy ohne sie gilt für Fremde genauso wie für angemeldete Besucher.
  • Wenn eine Tabelle die Einstellung an hat, keine Policies besitzt und deine App ihre Daten trotzdem zeigt, finde heraus, was sie umgeht, bevor du sonst etwas anfasst.

Fang mit der Tabelle an, die dir am peinlichsten wäre, wenn sie eine öffentliche Seite wäre, und bring die heute in Ordnung. Die 10-Minuten-Sicherheitscheckliste deckt das zusammen mit dem Übrigen ab, was bei einer frisch gestarteten App einen Blick wert ist, und der Supabase-Sicherheitsleitfaden geht durch, was sonst noch offen zu bleiben pflegt.

FAQ

Ich habe Row Level Security angeschaltet und meine App zeigt keine Daten mehr. Habe ich etwas kaputt gemacht?

Nein, so soll die Einstellung wirken. Mit Row Level Security an und ohne geschriebene Policies weist Postgres (die Datenbank-Engine unter Supabase) standardmäßig jede Anfrage ab, auch die deiner eigenen App. Die Lösung ist eine Policy, die beschreibt, wer welche Zeilen sehen darf. Der Fehler, den es zu vermeiden gilt: eine Policy, die alle erlaubt. Genau die bringt die App zurück und lässt die Tabelle offen.

Ist USING (true) jemals die richtige Policy?

Ja, für Daten, die wirklich öffentlich sind. Eine Tabelle mit veröffentlichten Blogbeiträgen, ein Produktkatalog, eine Liste von Orten auf einer Karte: Die sollen von allen gelesen werden können, und eine Policy, die genau das erlaubt, ist richtig. Sie hört in dem Moment auf richtig zu sein, in dem Menschen in der Tabelle stehen. Frag dich, ob du den Inhalt dieser Tabelle auf einer öffentlichen Seite stehen lassen würdest, und lass die Antwort die Policy entscheiden.

Schützt Row Level Security mich, wenn mein geheimer Schlüssel abgeflossen ist?

Nein. Ein geheimer Supabase-Schlüssel (sb_secret_ in neueren Projekten, service_role in älteren) umgeht Row Level Security vollständig, dafür ist er gedacht. Jede Policy, die du geschrieben hast, wird übersprungen, auf jeder Tabelle. Liegt dieser Schlüssel in deinem Frontend, tun deine Policies nichts für dich, und den Schlüssel zu rotieren ist die erste Aufgabe, vor jeder Arbeit an den Regeln.

Brauche ich Row Level Security, wenn meine App schon einen Login hat?

Ja. Dein Login steuert deine App, und deine App ist nicht der einzige Weg zu deiner Datenbank. Supabase gibt jedem Projekt eine Webadresse, die Anfragen direkt beantwortet, und der Schlüssel dafür steckt in dem Code, den deine App an jeden Besucher ausliefert. Policies sind der Teil, der unabhängig davon greift, durch welche Tür die Anfrage kam.

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

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.