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.
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 Security | Die Policy | Deine App | Ein Fremder mit deinem öffentlichen Supabase-Schlüssel |
|---|---|---|---|
| Aus | egal, keine wird geprüft | Läuft | Liest jede Zeile |
| An | keine geschrieben | Kaputt | Liest nichts |
| An | USING (true) | Läuft | Liest jede Zeile |
| An | USING (auth.uid() = user_id) | Läuft | Liest nur die eigenen |
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
trueist. 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_…, oderservice_rolein 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.
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, dieauth.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.