Leitfaden
Supabase Row Level Security richtig einsetzen
Bei Supabase steht der öffentliche Schlüssel (anon key oder publishable key) absichtlich im Frontend. Wer Ihre App öffnet, kann ihn auslesen und damit direkt die Datenbank-API ansprechen. Was er dort sieht und ändern darf, entscheidet allein Row Level Security (RLS): Ist RLS auf einer Tabelle ausgeschaltet oder lässt eine Policy alles durch, sind die Daten für jeden offen. Supabase RLS richtig einsetzen heißt deshalb, RLS auf jeder Tabelle einzuschalten, je Operation eine Policy mit Bezug auf auth.uid() zu schreiben und das Ergebnis mit einem zweiten Konto zu prüfen.
- Der öffentliche Supabase-Schlüssel ist kein Geheimnis. Laut Supabase-Dokumentation erreicht er nur, was RLS zulässt. Die Sicherheit Ihrer Daten hängt also an den Policies, nicht am Schlüssel.
- Der geheime Schlüssel (service_role oder secret key) umgeht jede Policy. Er gehört nie in den Browser, in eine App oder in ein Repository.
- Die häufigsten Fehler: RLS nicht eingeschaltet, Policies mit
true, zu weite Schreibrechte, öffentliche Storage-Buckets, Views und Funktionen, die RLS umgehen. - Mit KI-Werkzeugen gebaute Apps funktionieren für den Ersteller oft einwandfrei, auch wenn die Policies fehlen. Der Fehler fällt erst auf, wenn jemand mit einem fremden Konto oder ganz ohne Anmeldung fragt.
Wie Supabase schützt
Öffentlicher Schlüssel, Rollen, Policies.
Zum AbschnittTypische Fehler
Von fehlender RLS bis zum offenen Bucket.
Zum AbschnittMit KI gebaute Apps
Warum der Fehler unsichtbar bleibt.
Zum AbschnittPolicy-Beispiele
Minimal und korrekt, zum Übernehmen.
Zum AbschnittSelbst prüfen
Security Advisor und Test mit zweitem Konto.
Zum AbschnittCheckliste
Alle Punkte vor dem Livegang.
Zum AbschnittWie Supabase Zugriffe schützt
Supabase stellt Ihre Postgres-Datenbank über eine automatisch erzeugte API bereit, erreichbar unter https://<projekt>.supabase.co/rest/v1/. Ihre App spricht diese API direkt aus dem Browser an, ohne eigenen Server dazwischen. Dafür braucht sie einen Schlüssel, und der steht zwangsläufig im ausgelieferten JavaScript.
Das ist gewollt. Die Supabase-Dokumentation zu API-Schlüsseln nennt den publishable key ausdrücklich „sicher, um ihn online zu zeigen“, etwa auf einer Webseite oder im Quellcode, und begründet das so: Jeder kann ihn lesen, deshalb erreicht er nur, was Row Level Security erlaubt. Der ältere anon key ist laut Dokumentation die Vorgängerversion des publishable key. Supabase hat angekündigt, die bisherigen Schlüssel anon und service_role durch publishable und secret keys abzulösen.
Jede Anfrage läuft in Postgres unter einer Rolle: anon für Besucher ohne Anmeldung, authenticated für angemeldete Nutzer. Zwei Prüfungen entscheiden dann über den Zugriff:
- Grants legen fest, ob eine Rolle eine Operation auf einer Tabelle überhaupt ausführen darf, also lesen, anlegen, ändern oder löschen.
- Policies legen fest, auf welche Zeilen sich diese Operation erstreckt. Sie greifen nur, wenn RLS auf der Tabelle eingeschaltet ist.
Die Dokumentation weist darauf hin, dass in bestehenden Projekten eine neue Tabelle im Schema public mit allen Rechten für anon und authenticated startet. Ohne RLS bedeutet das: Wer die Projekt-URL und den öffentlichen Schlüssel kennt, darf lesen, ändern und löschen.
Ist RLS eingeschaltet und gibt es noch keine Policy, gilt das Gegenteil: Über die API ist dann nichts mehr zu sehen, bis Sie Policies anlegen. Postgres arbeitet in diesem Fall mit einer Standard-Ablehnung. Tabellen, die Sie im Supabase-Dashboard anlegen, haben RLS laut Dokumentation bereits eingeschaltet. Tabellen aus dem SQL-Editor, aus Migrationen oder aus anderen Werkzeugen müssen Sie selbst absichern.
Die typischen Fehler
Die folgenden Fehler sind keine Schwächen von Supabase, sondern Konfigurationen, die genau so wirken, wie sie geschrieben sind. Die meisten meldet der Security Advisor.
1. RLS ist nicht eingeschaltet
Der folgenschwerste Fall: RLS ist aus, die Grants sind vorhanden, der öffentliche Schlüssel reicht für vollen Zugriff. Typische Ursache ist eine Tabelle, die per SQL oder Migration angelegt wurde, wo RLS nicht automatisch aktiv ist. Ein Sonderfall: Es gibt Policies, aber RLS ist ausgeschaltet. Die Policies sehen dann nach Schutz aus, wirken aber nicht.
2. Policies, die alles erlauben
Eine Policy mit using (true) oder with check (true) lässt jede Zeile durch. Für wirklich öffentliche Inhalte wie veröffentlichte Blogbeiträge ist das in Ordnung, für Kundendaten nicht. Supabase beschreibt diese Konstellation in seiner Prüfregel als „häufige Fehlkonfiguration“ und nennt als Ursachen Platzhalter aus der Entwicklung, die nie ersetzt wurden, Kopierfehler aus Beispielen und die Annahme, andere Policies würden schon einschränken.
Diese Annahme ist falsch. Mehrere Policies für dieselbe Operation werden in Postgres standardmäßig mit ODER verknüpft. Eine einzige zu weite Policy hebelt damit alle engeren aus. Supabase empfiehlt zudem je eine eigene Policy für select, insert, update und delete.
3. Der geheime Schlüssel im Frontend
Der service_role key, in der neuen Form der secret key, gehört zur Rolle service_role. Diese Rolle hat das Recht, RLS zu umgehen. Die Dokumentation sagt unmissverständlich: Ein secret key umgeht jede Policy und gehört nie in einen Browser, in eine ausgelieferte App oder in die Versionsverwaltung. Neue secret keys werden zwar abgewiesen, wenn sie erkennbar aus einem Browser kommen, laut Supabase anhand des User-Agent. Aus anderen Werkzeugen funktioniert ein abgeflossener Schlüssel aber weiterhin. Lag er je im Frontend oder in einem öffentlichen Repository, ersetzen Sie ihn und deaktivieren oder löschen Sie den alten.
4. Policies, die nur das Lesen regeln
Eine saubere select-Policy schützt das Lesen. Schreibzugriffe brauchen eigene Regeln, und dort liegen die feineren Fehler:
- Anlegen ohne Prüfung des Besitzers. Fehlt bei
insertdie Bedingungwith check ((select auth.uid()) = user_id), kann ein Nutzer Zeilen im Namen eines anderen anlegen. - Ändern ohne Grenze für das Ergebnis. Bei
updateentscheidetusing, welche Zeilen geändert werden dürfen, undwith check, wie die Zeile danach aussehen darf. Ohne diese Grenze kann jemand eine eigene Zeile einem anderen Konto zuschieben. - Zu viele änderbare Spalten. RLS regelt Zeilen, nicht Spalten. Darf ein Nutzer seine Zeile ändern, darf er grundsätzlich jede Spalte darin ändern, also auch Felder wie
role,is_adminoder einen Zahlungsstatus. Solche Felder gehören in eine eigene Tabelle, die Nutzer nicht beschreiben dürfen.
Ein Detail, das oft zu Fehlersuche führt: Damit update wie erwartet funktioniert, braucht die Tabelle laut Dokumentation auch eine passende select-Policy. Wer dann, um die App „zum Laufen zu bringen“, eine weite Policy ergänzt, öffnet oft mehr, als er wollte.
5. Der Vergleich mit auth.uid() fehlt oder ist falsch
auth.uid() liefert die ID des angemeldeten Nutzers. Eine Policy, die nur prüft, ob jemand angemeldet ist (to authenticated using (true)), gibt jedem Konto alle Zeilen, auch fremde. Bei Apps mit freier Registrierung ist das praktisch dasselbe wie öffentlich. Der Vergleich mit der Besitzer-Spalte gehört in jede Policy für personenbezogene Daten. Zwei weitere Punkte aus der Dokumentation:
- Ohne Anmeldung gibt
auth.uid()nullzurück.auth.uid() = user_idist dann nie wahr, was gewollt ist. Supabase empfiehlt, die Absicht trotzdem ausdrücklich zu schreiben und immer die Rolle mittoanzugeben. - Anonyme Anmeldungen in Supabase Auth laufen unter der Rolle
authenticated, nichtanon. Wer sie einschaltet, sollte in Policies über den JWT-Claimis_anonymousunterscheiden, wo das nötig ist.
6. Berechtigungen aus user_metadata
Die Metadaten in raw_user_meta_data kann der angemeldete Nutzer laut Dokumentation selbst ändern. Eine Policy, die Rollen oder Teamzugehörigkeit aus user_metadata liest, lässt sich deshalb vom Nutzer aushebeln. Für Berechtigungen eignet sich app_metadata, die der Nutzer nicht ändern kann, oder eine eigene Tabelle.
7. Öffentliche Storage-Buckets
Supabase Storage kennt öffentliche und private Buckets. Bei einem öffentlichen Bucket umgeht der Abruf von Dateien laut Dokumentation die Zugriffskontrolle: Wer die URL kennt, lädt die Datei. Hochladen, Löschen, Verschieben und Kopieren bleiben geschützt. Für Profilbilder ist ein öffentlicher Bucket passend, für Rechnungen, Ausweise oder Verträge nicht. Kommt zu einem öffentlichen Bucket noch eine weite select-Policy auf storage.objects hinzu, lässt sich der Inhalt zusätzlich auflisten. Auch dafür gibt es einen eigenen Prüfpunkt im Security Advisor.
8. Views und Funktionen, die RLS umgehen
Views umgehen RLS standardmäßig, weil sie in der Regel mit dem Nutzer postgres angelegt werden und dessen Rechte nutzen. Eine View über eine geschützte Tabelle gibt dann alle Zeilen heraus. Ab Postgres 15 lässt sich das mit security_invoker = true ändern. Ähnlich bei Funktionen mit security definer: Sie laufen mit den Rechten ihres Erstellers. Darf anon oder authenticated eine solche Funktion ausführen, ist sie über /rest/v1/rpc/<name> aufrufbar und kann Daten liefern, die RLS sonst zurückhalten würde.
Warum mit KI gebaute Apps besonders betroffen sein können
Werkzeuge wie Lovable, Bolt oder Cursor erzeugen aus einer Beschreibung in kurzer Zeit eine funktionierende App. Lovable bindet dafür nach eigener Darstellung Supabase als Backend an, und auch andere Werkzeuge können Supabase-Code erzeugen. Die Lovable-Dokumentation nennt falsch konfigurierte RLS-Regeln selbst „eine häufige Ursache für Datenlecks“. Das Risiko liegt weniger im erzeugten Code als im Prüfvorgang danach. Eine fehlende oder zu weite Policy ist im normalen Gebrauch unsichtbar:
- Der Ersteller testet mit seinem eigenen Konto. Er sieht seine eigenen Daten, alles funktioniert, auch wenn er ebenso fremde Daten sehen könnte.
- Fehlermeldungen entstehen, wenn RLS zu streng ist, nicht wenn sie fehlt. Wer eine Fehlermeldung an das Werkzeug zurückgibt, bekommt eine Lösung, die die App zum Laufen bringt. Ob diese Lösung den Zugriff unnötig weit öffnet, muss jemand gezielt prüfen.
- Im SQL-Editor des Dashboards erscheinen alle Zeilen, weil der Tabelleneigentümer und Rollen mit
BYPASSRLSnicht den Policies unterliegen. Ein Test dort sagt über RLS nichts aus.
Der öffentlich dokumentierte Fall: CVE-2025-48757
Im Mai 2025 wurde CVE-2025-48757 veröffentlicht. Der Eintrag beschreibt unzureichende Row-Level-Security-Policies in Lovable bis zum 15.04.2025, durch die nicht angemeldete Angreifer aus der Ferne beliebige Tabellen der erzeugten Anwendungen lesen oder beschreiben konnten. Der Eintrag ist als „disputed“ markiert: Der Anbieter bestreitet ihn mit der Begründung, dass jeder Kunde der Plattform die Verantwortung für den Schutz der Daten seiner Anwendung übernimmt.
Lovable selbst schreibt in seinem Blog vom Juni 2025, dass mit Supabase verbundene Apps auf RLS beruhen und fehlende oder zu weite Policies Daten offenlegen können. Nach eigener Angabe prüft Lovable Apps vor dem Veröffentlichen automatisch. Die Lovable-Dokumentation hält zugleich fest, dass die Werkzeuge keine vollständige Sicherheit garantieren können, dass Sie selbst dafür verantwortlich sind, dass Ihre App die für ihren Zweck passenden Anforderungen erfüllt, und empfiehlt für Apps mit sensiblen Daten eine zusätzliche professionelle Sicherheitsprüfung.
Policy-Beispiele: minimal und korrekt
Das folgende Muster deckt den häufigsten Fall ab: Jeder Nutzer sieht und bearbeitet nur seine eigenen Einträge. Es folgt der Struktur der Supabase-Dokumentation zu RLS. Die Tabelle notes hat eine Spalte user_id vom Typ uuid.
-- RLS einschalten und nur nötige Rechte vergeben
alter table public.notes enable row level security;
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;
-- Lesen: nur eigene Zeilen
create policy "notes_select_own"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );
-- Anlegen: nur mit der eigenen user_id
create policy "notes_insert_own"
on public.notes for insert
to authenticated
with check ( (select auth.uid()) = user_id );
-- Ändern: nur eigene Zeilen, und sie bleiben eigene
create policy "notes_update_own"
on public.notes for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
-- Löschen: nur eigene Zeilen
create policy "notes_delete_own"
on public.notes for delete
to authenticated
using ( (select auth.uid()) = user_id );
-- Index auf die Spalte, nach der die Policies filtern
create index on public.notes (user_id);
Drei Details stecken darin. (select auth.uid()) statt auth.uid() ist eine Empfehlung der Dokumentation für bessere Leistung. to authenticated sorgt dafür, dass die Policy für Besucher ohne Anmeldung gar nicht erst ausgewertet wird. Und die Grants beschränken anon zusätzlich, denn Policies allein nehmen bereits vergebene Rechte nicht zurück.
Für Dateien gilt dasselbe Prinzip auf der Tabelle storage.objects. Dieses Beispiel, angelehnt an die Storage-Dokumentation, erlaubt Uploads nur in einen Ordner, der der eigenen Nutzer-ID entspricht, in einem privaten Bucket:
create policy "uploads_own_folder"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'documents' and
(storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);
Für Lesen, Ändern und Löschen brauchen Sie entsprechende eigene Policies. Für Upserts, also das Überschreiben vorhandener Dateien, verlangt Supabase zusätzlich select und update.
So prüfen Sie Ihre App selbst
Prüfen Sie nur Projekte, die Ihnen gehören oder für die Sie eine Erlaubnis haben. Für die eigene App brauchen Sie dafür keine Spezialwerkzeuge.
Schritt 1: Security Advisor auswerten
Im Supabase-Dashboard finden Sie unter „Advisors“ den Security Advisor. Dieselben Prüfungen gibt es über die Kommandozeile mit supabase db advisors. Für RLS sind laut Dokumentation vor allem diese Prüfpunkte wichtig:
| Prüfpunkt | Bedeutung |
|---|---|
0013_rls_disabled_in_public | Tabelle im Schema public ohne RLS, öffentlich lesbar und beschreibbar |
0007_policy_exists_rls_disabled | Policies vorhanden, RLS aber ausgeschaltet |
0024_permissive_rls_policy | Policy mit immer wahrer Bedingung wie using (true) |
0015_rls_references_user_metadata | Policy stützt sich auf Daten, die der Nutzer selbst ändern kann |
0010_security_definer_view | View, die RLS umgeht |
0028 / 0029 | Security-Definer-Funktion, die anon bzw. authenticated aufrufen darf |
0025_public_bucket_allows_listing | Öffentlicher Bucket, dessen Inhalt sich auflisten lässt |
0023_sensitive_columns_exposed | Tabelle mit vermutlich sensiblen Spalten ohne Zugriffsbeschränkung über die API erreichbar |
Nicht jeder Treffer ist ein Fehler, manche Daten sollen öffentlich sein. Entscheiden Sie je Punkt bewusst.
Schritt 2: Mit einem zweiten Konto fremde Zeilen abfragen
Der aussagekräftigste Test ist einfach. Legen Sie zwei Testkonten an, A und B, und erzeugen Sie mit A einige Datensätze. Melden Sie sich dann als B an und versuchen Sie, an die Daten von A zu kommen, etwa in der Browser-Konsole Ihrer App mit dem Supabase-Client:
// angemeldet als Konto B
const { data } = await supabase.from('notes').select('*');
// erwartet: nur Zeilen von B
const { data: changed } = await supabase
.from('notes')
.update({ title: 'geändert von B' })
.eq('id', 'ID-EINER-ZEILE-VON-A')
.select();
// erwartet: leeres Ergebnis, Zeile von A unverändert
Wichtig beim Lesen des Ergebnisses: Wird eine Änderung durch die using-Bedingung blockiert, meldet Postgres keinen Fehler, sondern trifft einfach null Zeilen. Prüfen Sie daher immer das zurückgegebene Ergebnis und anschließend als A, ob die Zeile unverändert ist. Wiederholen Sie den Test ohne Anmeldung, versuchen Sie als B eine Zeile mit der user_id von A anzulegen und öffnen Sie Datei-URLs in einem privaten Browserfenster.
Schritt 3: Tests ins Projekt übernehmen
Supabase empfiehlt, für jede geschützte Tabelle eine Testdatei unter supabase/tests/ anzulegen, die für anon und authenticated erlaubte und verbotene Zugriffe auf alle vier Operationen prüft, und sie mit supabase test db auszuführen. So fällt auf, wenn eine spätere Änderung eine Policy aufweicht.
Checkliste vor dem Livegang
| Bereich | Maßnahme |
|---|---|
| RLS | Auf jeder Tabelle in einem über die API erreichbaren Schema eingeschaltet, auch bei per SQL oder Migration angelegten Tabellen |
| Grants | Für anon und authenticated nur die Operationen, die die App braucht |
| Policies | Je Operation eine eigene Policy, mit to-Rolle und Vergleich auf (select auth.uid()) |
| Schreiben | with check bei insert und update; Rollen, Rechte und Zahlungsstatus nicht vom Nutzer änderbar |
| Offene Policies | using (true) nur für wirklich öffentliche Daten |
| Schlüssel | Im Frontend nur publishable bzw. anon key; secret bzw. service_role key nur serverseitig |
| Metadaten | Keine Berechtigungen aus user_metadata |
| Storage | Sensible Dateien nur in privaten Buckets, Policies auf storage.objects |
| Views und Funktionen | Views mit security_invoker = true; Security-Definer-Funktionen nicht für anon freigegeben |
| Kontrolle | Security Advisor ohne ungeklärte Treffer, Test mit zweitem Konto und ohne Anmeldung bestanden |
Was ein Advisor-Scan nicht findet
Der Security Advisor weiß nicht, dass in Ihrer App nur Mitglieder desselben Teams eine Rechnung sehen dürfen, dass ein Nutzer seinen Tarif nicht selbst hochsetzen darf oder dass eine Edge Function eine Prüfung überspringt. Solche Fehler in der Zugriffslogik zeigen sich erst, wenn jemand die App gezielt mit mehreren Rollen angreift. Werden personenbezogene Daten dabei tatsächlich abgerufen, kann das eine Meldepflicht nach Art. 33 DSGVO auslösen. Ob das im Einzelfall zutrifft, ist eine Rechtsfrage. Dieser Artikel ist keine Rechtsberatung.
Ist Ihre Supabase-App wirklich dicht?
Wir prüfen Apps auf Supabase, auch mit KI-Werkzeugen gebaute, im schriftlich vereinbarten Umfang: RLS und Grants auf allen Tabellen, Schreibrechte und Spalten mit Berechtigungsfolgen, Storage-Buckets, Views, Funktionen und Edge Functions, jeweils mit mehreren Testkonten und ohne Anmeldung. Jeder Befund kommt mit einem nachvollziehbaren Nachweis und einem konkreten Korrekturvorschlag. Mehr dazu unter SaaS-Penetrationstest.
App-Sicherheitscheck anfragenCyberSec42 ist eine unabhängige technische Sicherheitsprüfung, keine akkreditierte Zertifizierungsstelle und keine Rechtsberatung. CyberSec42 steht in keiner Verbindung zu Supabase oder Lovable.
Häufige Fragen
Darf der Supabase anon key im Frontend stehen?
Ja. Der anon key und sein Nachfolger, der publishable key, sind laut Supabase dafür gedacht, öffentlich zu sein. Sie erreichen nur, was Row Level Security erlaubt. Die Voraussetzung ist, dass RLS auf allen über die API erreichbaren Tabellen eingeschaltet ist und die Policies stimmen.
Was passiert, wenn der service_role key öffentlich wird?
Der service_role key, heute secret key, umgeht alle RLS-Policies. Wer ihn hat, kann alle Daten lesen und ändern. Ersetzen Sie ihn sofort durch einen neuen Schlüssel, deaktivieren oder löschen Sie den alten und prüfen Sie die Logs auf unerwartete Zugriffe.
Ist RLS bei neuen Supabase-Tabellen automatisch aktiv?
Bei Tabellen, die Sie im Supabase-Dashboard anlegen, ja. Bei Tabellen aus dem SQL-Editor, aus Migrationen oder aus anderen Werkzeugen müssen Sie RLS laut Dokumentation selbst einschalten.
Reicht der Security Advisor als Prüfung?
Er ist ein guter erster Schritt und findet fehlende RLS, offensichtlich offene Policies und riskante Views oder Funktionen. Ob Ihre Policies zur Logik Ihrer App passen, etwa wer welche Zeile ändern darf, prüft er nicht. Dafür braucht es Tests mit mehreren Konten.
Sind mit Lovable gebaute Apps unsicher?
Nicht grundsätzlich. Lovable prüft Apps nach eigener Angabe vor dem Veröffentlichen auf häufige Probleme, darunter fehlende RLS. Die Verantwortung für korrekte Zugriffsregeln liegt laut Lovable aber beim Betreiber der App. Prüfen Sie Ihre Policies deshalb selbst, wie in diesem Artikel beschrieben.
Weiterlesen
- Vibe Coding Sicherheit: die Checkliste
- Lovable-Sicherheit: So machen Sie Ihre Lovable-App sicher
- Firebase Security Rules richtig schreiben
- Penetrationstest: Ablauf, Dauer und was ihn kostet
- Schwachstellenanalyse: was sie ist und wann Sie eine brauchen
- KI-Agent-Sicherheit: der Überblick
- Leistung: SaaS-Penetrationstest
Quellen
- Supabase Docs: Row Level Security
- Supabase Docs: Understanding API keys
- Supabase Docs: Securing your API
- Supabase Docs: Tables and Data
- Supabase Docs: Database Advisors
- Supabase Docs: Storage Buckets
- Supabase Docs: Storage Access Control
- PostgreSQL Docs: Row Security Policies
- NVD: CVE-2025-48757
- CVE.org: CVE-2025-48757
- Lovable Blog: Secure vibe coding
- Lovable Docs: Security
- EUR-Lex: Datenschutz-Grundverordnung (EU) 2016/679