CVE-2025-48757 ist eine als kritisch eingestufte Lovable-Schwachstelle (CVSS 9.3), die mehr als 170 Anwendungen offengelegt hat. Das Problem: Jeder konnte ohne Login Daten in deiner Datenbank lesen, und in vielen Fällen auch ändern: Namen, E-Mails, API-Keys von Drittanbietern, sogar den Zahlungsstatus. Die Ursache ist kein versteckter Lovable-Bug, sondern ein Muster, das fast alle „vibe-coded" Apps teilen: die gesamte Datenbanksicherheit den RLS-Policies von Supabase zu überlassen… und sie unvollständig zu lassen. Dieser Guide erklärt, was genau passiert ist, wie du in fünf Minuten prüfst, ob deine App dasselbe Loch hat, wie du es mit konkretem SQL schließt und was zu tun ist, wenn du bereits Daten offengelegt hast.
Was wirklich passiert ist
Lovable baut Apps, in denen der Browser direkt per REST-Aufrufen mit der Datenbank (Supabase) spricht, mit dem öffentlichen anon-Key. Das ist ein solides Design, solange jede Tabelle Row-Level-Security-Policies (RLS) hat, die entscheiden, welche Zeilen jeder Nutzer lesen oder anfassen darf. Die Schwachstelle entstand, weil viele Projekte mit Tabellen ohne RLS oder mit unzureichendem RLS live gingen. Ein Angreifer musste nur die REST-Anfrage bearbeiten, einen Filter in der URL ändern, um „alle Zeilen" einer Tabelle anzufordern, die eigentlich geschützt sein sollte.
Um zu verstehen, warum das Bearbeiten einer URL reicht, musst du wissen, wie Supabase unter der Haube funktioniert. Vor deiner Postgres-Datenbank sitzt eine Schicht namens PostgREST, die jede Tabelle in einen HTTP-Endpoint verwandelt: GET /rest/v1/customers?select=* liefert Zeilen aus der Tabelle customers. Der anon-Key ist kein Geheimnis: Er steckt im JavaScript deiner App und jeder kann ihn sehen. Das Einzige, was entscheidet, welche Zeilen aus diesem Endpoint kommen, sind die RLS-Policies der Tabelle. Ohne sie ist die Antwort die ganze Tabelle.
Gemeldet wurde sie im März 2025 von Matt Palmer und Kody Low sowie unabhängig von Danial Asaria, mit öffentlicher Bekanntgabe am 29. Mai 2025 (technische Analyse, CVE-Eintrag). Jedes Lovable-Projekt mit einer am oder vor dem 15. April 2025 erstellten Datenbank kann betroffen sein.
Zeitleiste in Kürze
- März 2025: erste private Meldungen an Lovable; veröffentlichte Apps mit Tabellen, die ohne Session lesbar sind, werden gefunden.
- 15. April 2025: der Stichtag, den Lovable für betroffene Projekte nennt; danach generierte Apps enthalten Änderungen daran, wie Tabellen angelegt werden.
- 29. Mai 2025: öffentliche Bekanntgabe und CVE-Vergabe mit kritischer Bewertung.
- Danach: Lovable fügt Sicherheitswarnungen im Editor hinzu, aber die Policies bleiben in der Verantwortung dessen, der die App veröffentlicht.
Warum es dich betrifft, auch wenn du Lovable nicht nutzt
Das ist kein reines Lovable-Problem. Jede App, die direkt aus dem Browser mit Supabase spricht, ob mit Bolt, v0, Cursor, Replit oder von Hand gebaut, hängt davon ab, dass RLS auf jeder Tabelle korrekt ist. Die App sieht perfekt aus, der Login funktioniert, aber die echte Sicherheit lebt in der Datenbank, nicht in der Oberfläche. Einen Button im Frontend zu verstecken schützt nichts: Die REST-Anfrage ist weiterhin da für jeden, der weiß, wo er suchen muss.
Dasselbe gilt für Firebase mit Regeln im „Testmodus", für PocketBase ohne Collection-Regeln oder für jedes Backend-as-a-Service, das der Browser direkt aufruft. Das Muster ist immer gleich: Die KI, die deine App generiert hat, weiß, wie man sie zum Laufen bringt, schreibt aber selten die Regeln, die verhindern, dass sie für die Falschen funktioniert.
So findest du heraus, ob deine App verwundbar ist (5 Minuten)
Test 1: die Inkognito-Anfrage
- Öffne die DevTools (F12) → Tab Network, während du deine App benutzt, und beobachte die Aufrufe an
*.supabase.co/rest/v1/. Kopiere eine dieser URLs vollständig (derapikeyreist in einem Header; in der URL siehst du Tabelle und Filter). - Öffne sie in einem Inkognito-Fenster, ohne Session. Liefert sie Daten, bekommt sie jeder Besucher auch.
- Versuch, mehr zu verlangen: Liefert ein Aufruf deine Daten, entferne den Filter aus der URL (den Teil
?user_id=eq.…). Gibt er die Zeilen anderer Nutzer heraus, ist dein RLS nicht wirksam.
Test 2: das Supabase-Dashboard
Öffne dein Projekt → Table Editor. Jede Tabelle ohne RLS trägt ein rotes „RLS disabled"-Badge. Geh dann zu Authentication → Policies: Eine Tabelle mit aktivem RLS, aber ohne Policy verweigert alles (gut für die Daten, aber deine App funktioniert nicht mehr), und eine mit einer using (true)-Policy ist so offen, als wäre RLS aus. Das Dashboard hat außerdem einen Security Advisor (unter Advisors), der genau diese Tabellen auflistet.
Test 3: eine SQL-Abfrage
Im SQL Editor des Dashboards listet diese Abfrage die Tabellen im Schema public (das, was die API exponiert) auf, bei denen RLS nicht aktiviert ist:
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind = 'r'
and not c.relrowsecurity;
Liefert sie eine Zeile mit echten Daten, ist diese Tabelle in diesem Moment über die API offen.
So behebst du es, Schritt für Schritt
1. Aktiviere RLS auf jeder Tabelle
Nicht nur auf den „wichtigen". Die Regel lautet standardmäßig verweigern: Ohne eine Policy, die es erlaubt, kommt niemand rein. Für jede Tabelle im Schema public:
alter table public.customers enable row level security;
Sobald du das tust, sieht deine App keine Daten mehr, bis du Policies schreibst. Das ist so gewollt: Die Tür ist jetzt zu, und du öffnest gleich nur das, was nötig ist.
2. Schreib explizite Policies pro Operation
Das häufigste Muster ist „jeder Nutzer sieht und bearbeitet nur seine eigenen Zeilen", indem auth.uid() mit der Spalte verglichen wird, die den Besitzer speichert. Für eine Tabelle customers mit einer Spalte user_id:
create policy "customers: eigene lesen"
on public.customers for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "customers: eigene einfügen"
on public.customers for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "customers: eigene ändern"
on public.customers for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "customers: eigene löschen"
on public.customers for delete
to authenticated
using ((select auth.uid()) = user_id);
Drei Details machen den Unterschied: to authenticated hält anonyme Besucher draußen; with check verhindert, dass jemand Zeilen im Namen eines anderen einfügt oder bearbeitet (ohne es könnte ein Nutzer Daten mit der user_id eines anderen anlegen); und auth.uid() in ein select zu packen lässt Postgres es einmal pro Abfrage statt einmal pro Zeile auswerten, was bei großen Tabellen die Performance vervielfacht. Leg einen Index auf user_id an, falls du keinen hast.
3. Nutze den service_role-Key niemals im Frontend
Er umgeht RLS komplett: Mit ihm existiert keine der Policies, die du gerade geschrieben hast. Dieser Key lebt nur auf dem Server (Edge Functions, dein eigenes Backend, ein Worker). War er jemals im Client-Code, betrachte ihn als geleakt und rotiere ihn im Dashboard. Supabase bietet inzwischen auch neuere Keys an (sb_publishable_… für den Browser und sb_secret_… für den Server), die die Trennung offensichtlicher machen.
4. Teste erneut
Wiederhole den Inkognito-Test: Die Anfrage ohne Session muss eine leere Liste oder einen 401-Fehler liefern. Logge dich dann mit zwei verschiedenen Konten ein und bestätige, dass keines die Daten des anderen sieht. Wenn du automatisierte Tests hast, ist das der Fall, der einen verdient.
Häufige Fehler beim Beheben
- Policies mit
using (true). KI-Assistenten schlagen sie oft vor, „damit es läuft". Sie lassen die Tabelle so offen wie ganz ohne RLS. with checkvergessen. Es schützt Lesezugriffe, erlaubt aber Schreibzugriffe im Namen anderer.- „Katalog"-Tabellen mit personenbezogenen Daten. Eine öffentliche Profiltabelle, die Namen zeigen soll, legt am Ende E-Mails oder Telefonnummern offen. Exponiere nur die nötigen Spalten über eine View.
- Views, die RLS umgehen. In Postgres läuft eine View mit den Rechten dessen, der sie erstellt hat. Aus dem Dashboard erstellt, kann sie Zeilen zurückgeben, die die Policies der Basistabelle verbieten würden. Erstelle Views mit
security_invoker = true(Postgres 15 oder neuer) oder prüfe sie einzeln. - Storage ohne Policies. Buckets haben eigene Regeln. Ein öffentlicher Bucket liefert jede Datei an jeden mit der URL; private Dokumente gehören in private Buckets mit Policies pro Nutzer.
- Nur die Produktion reparieren. Wenn du ein Entwicklungsprojekt mit kopierten Daten hast, ist es genauso exponiert.
Wenn du bereits Daten offengelegt hast
Schließ zuerst die Tür (die Schritte oben). Rotiere dann jeden Key, der in lesbaren Tabellen lag: API-Keys von Drittanbietern, Tokens, Webhooks. Sieh dir die API-Logs im Supabase-Dashboard an, um abzuschätzen, was gelesen wurde und seit wann. Enthalten die Daten personenbezogene Informationen europäischer Nutzer, verpflichtet dich die DSGVO zu prüfen, ob die Datenschutzbehörde innerhalb von 72 Stunden informiert werden muss und, bei hohem Risiko, die Nutzer selbst. Dokumentiere, was du gefunden und was du getan hast: Danach wird man dich fragen, wenn jemand anklopft.
Ein Prompt, um es mit deiner KI zu beheben
Wenn du mit Cursor, Claude oder dem Chat von Lovable selbst arbeitest, liefert dieser Prompt meist ein gutes Ergebnis: „Liste alle Tabellen im Schema public meines Supabase-Projekts auf. Aktiviere für jede Row-Level Security und schreibe getrennte Policies für select, insert, update und delete, beschränkt auf die Rolle authenticated, sodass jeder Nutzer nur auf Zeilen zugreift, deren Spalte user_id gleich (select auth.uid()) ist. Verwende with check bei insert und update. Sag mir, welche Tabellen keine Besitzerspalte haben, damit ich die von Hand entscheide. Verwende niemals using (true)."
Wie Scanaris das prüft
Scanaris scannt deine Website schreibgeschützt und erkennt den Fingerabdruck dieses Musters: aus dem Browser erreichbare Supabase-Endpoints, freizügiges CORS, in deinem JavaScript exponierte Keys und Tokens sowie Daten, die die App ohne Session ausliefert. Gibst du einen schreibgeschützten Connection String an, prüft es außerdem Tabelle für Tabelle, ob RLS aktiv ist und ob Policies existieren. Füge deine URL ein und es sagt dir in Sekunden und nach Schweregrad sortiert, was offen daliegt, bevor es jemand anderes findet.
Häufig gestellte Fragen
Hat Lovable das schon behoben?
Lovable hat geändert, wie es Tabellen generiert, und Warnungen hinzugefügt, aber eine vor dem 15. April 2025 veröffentlichte App bleibt so, wie du sie hinterlassen hast: Policies fügen sich nicht von selbst hinzu. Und in jedem Projekt, neu oder alt, braucht jede Tabelle, die du danach anlegst, ihre eigenen.
Macht das Aktivieren von RLS meine App kaputt?
Ohne Policies ja: Alles wird verweigert. Das ist das korrekte Verhalten. Schreib die Policies aus Schritt 2 und die App funktioniert wieder, aber nur für die, die sie nutzen sollen.
Kann ich die Tabelle vom Frontend aus schützen?
Nein. Das Frontend ist Code, der auf dem Rechner des Besuchers läuft und beliebig verändert werden kann. Das Einzige, was sich nicht umgehen lässt, sind die Regeln, die die Datenbank durchsetzt.
Was, wenn meine App keine Nutzer hat, nur eine öffentliche Inhaltstabelle?
Dann ist eine öffentliche Lese-Policy legitim: for select to anon, authenticated using (true). Was es nicht geben darf, ist eine offene Schreib-Policy oder Spalten mit personenbezogenen Daten in dieser Tabelle.