← Terug naar de blog
BeveiligingScanaris Team · 8 min lezen · October 03, 2026

Lovable en de RLS-kwetsbaarheid (CVE-2025-48757): waarom jouw app misschien data lekt

CVE-2025-48757 is een als kritiek beoordeeld lek in Lovable (CVSS 9.3) waardoor meer dan 170 applicaties blootlagen. Het probleem: iedereen kon zonder in te loggen data in je database lezen, en in veel gevallen ook wijzigen: namen, e-mailadressen, API-keys van derden, zelfs de betaalstatus. De oorzaak is geen verborgen bug in Lovable, maar een patroon dat bijna elke "vibe-coded" app deelt: alle databasebeveiliging overlaten aan de RLS-policies van Supabase… en die onvolledig laten. Deze gids behandelt wat er precies gebeurde, hoe je in vijf minuten controleert of jouw app hetzelfde gat heeft, hoe je het dicht met concrete SQL en wat je doet als je al data hebt blootgesteld.

Wat er echt gebeurde

Lovable bouwt apps waarin de browser rechtstreeks met de database (Supabase) praat via REST-aanroepen, met de publieke anon-key. Dat is een gezond ontwerp zolang elke tabel Row-Level Security-policies (RLS) heeft die bepalen welke rijen elke gebruiker mag lezen of aanraken. Het lek ontstond doordat veel projecten live gingen met tabellen zonder RLS of met onvoldoende RLS. Een aanvaller hoefde alleen de REST-aanvraag aan te passen, een filter in de URL te veranderen, om "alle rijen" op te vragen van een tabel die beschermd had moeten zijn.

Om te begrijpen waarom het aanpassen van een URL genoeg is, moet je weten hoe Supabase onder de motorkap werkt. Voor je Postgres-database zit een laag genaamd PostgREST die elke tabel omzet in een HTTP-endpoint: GET /rest/v1/customers?select=* geeft rijen uit de tabel customers terug. De anon-key is geen geheim: hij zit in de JavaScript van je app en iedereen kan hem zien. Het enige dat bepaalt welke rijen uit dat endpoint komen, zijn de RLS-policies van de tabel. Zonder die policies is het antwoord de hele tabel.

Het werd in maart 2025 gemeld door Matt Palmer en Kody Low, en onafhankelijk door Danial Asaria, met publieke bekendmaking op 29 mei 2025 (technische analyse, CVE-vermelding). Elk Lovable-project met een database die op of vóór 15 april 2025 is aangemaakt, kan getroffen zijn.

Tijdlijn in het kort

  • Maart 2025: eerste privémeldingen aan Lovable; gepubliceerde apps worden gevonden met tabellen die zonder sessie leesbaar zijn.
  • 15 april 2025: de peildatum die Lovable noemt voor getroffen projecten; apps die daarna zijn gegenereerd bevatten wijzigingen in hoe tabellen worden aangemaakt.
  • 29 mei 2025: publieke bekendmaking en toekenning van de CVE met een kritieke score.
  • Daarna: Lovable voegt beveiligingswaarschuwingen toe in de editor, maar de policies blijven de verantwoordelijkheid van wie de app publiceert.

Waarom het jou raakt, ook als je Lovable niet gebruikt

Dit is geen probleem van Lovable alleen. Elke app die rechtstreeks vanuit de browser met Supabase praat, of je hem nu met Bolt, v0, Cursor, Replit of met de hand hebt gebouwd, is afhankelijk van correcte RLS op elke tabel. De app ziet er perfect uit, inloggen werkt, maar de echte beveiliging zit in de database, niet in de interface. Een knop verbergen in de frontend beschermt niets: de REST-aanvraag is er nog steeds voor wie weet waar hij moet kijken.

Hetzelfde geldt voor Firebase met regels in "testmodus", voor PocketBase zonder collectieregels, of voor elke backend-as-a-service die de browser rechtstreeks aanroept. Het patroon is altijd hetzelfde: de AI die je app genereerde weet hoe hij hem werkend krijgt, maar schrijft zelden de regels die voorkomen dat hij werkt voor de mensen voor wie hij niet bedoeld is.

Zo weet je of je app kwetsbaar is (5 minuten)

Test 1: de incognito-aanvraag

  • Open de DevTools (F12) → tabblad Network terwijl je je app gebruikt en let op de aanroepen naar *.supabase.co/rest/v1/. Kopieer een van die URL's volledig (de apikey reist mee in een header; in de URL zie je de tabel en het filter).
  • Open hem in een incognitovenster, zonder sessie. Geeft hij data terug, dan krijgt elke bezoeker die ook.
  • Probeer meer te vragen: geeft een aanroep jouw data terug, haal dan het filter uit de URL (het deel ?user_id=eq.…). Levert hij rijen van andere gebruikers op, dan is je RLS niet effectief.

Test 2: het Supabase-dashboard

Open je project → Table Editor. Elke tabel zonder RLS draagt een rode badge "RLS disabled". Ga daarna naar Authentication → Policies: een tabel met RLS aan maar zonder policy weigert alles (goed voor de data, maar je app werkt niet meer), en een tabel met een using (true)-policy staat net zo open als zonder RLS. Het dashboard heeft ook een Security Advisor (onder Advisors) die precies deze tabellen opsomt.

Test 3: één SQL-query

In de SQL Editor van het dashboard geeft deze query de tabellen in het schema public (het schema dat de API blootstelt) waarop RLS niet aanstaat:

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;

Geeft hij een rij terug met echte data, dan staat die tabel op dit moment open via de API.

Zo los je het op, stap voor stap

1. Zet RLS aan op elke tabel

Niet alleen op de "belangrijke". De regel is standaard weigeren: zonder policy die het toestaat, komt niemand binnen. Voor elke tabel in het schema public:

alter table public.customers enable row level security;

Zodra je dat doet, ziet je app geen data meer tot je policies schrijft. Dat is de bedoeling: de deur zit nu dicht en je gaat alleen openzetten wat nodig is.

2. Schrijf expliciete policies per operatie

Het meest voorkomende patroon is "elke gebruiker ziet en bewerkt alleen zijn eigen rijen", door auth.uid() te vergelijken met de kolom die de eigenaar opslaat. Voor een tabel customers met een kolom user_id:

create policy "customers: eigen lezen"
  on public.customers for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "customers: eigen invoegen"
  on public.customers for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

create policy "customers: eigen bijwerken"
  on public.customers for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);

create policy "customers: eigen verwijderen"
  on public.customers for delete
  to authenticated
  using ((select auth.uid()) = user_id);

Drie details maken het verschil: to authenticated houdt anonieme bezoekers buiten; with check voorkomt dat iemand rijen invoegt of bewerkt namens een ander (zonder zou een gebruiker data kunnen aanmaken met de user_id van iemand anders); en auth.uid() in een select zetten laat Postgres het één keer per query evalueren in plaats van één keer per rij, wat op grote tabellen de prestaties vermenigvuldigt. Voeg een index op user_id toe als je die niet hebt.

3. Gebruik de service_role-key nooit in de frontend

Die omzeilt RLS volledig: met die key bestaat geen enkele policy die je net hebt geschreven. Die key leeft alleen op de server (Edge Functions, je eigen backend, een worker). Heeft hij ooit in clientcode gestaan, beschouw hem dan als gelekt en roteer hem vanuit het dashboard. Supabase biedt inmiddels ook nieuwere keys (sb_publishable_… voor de browser en sb_secret_… voor de server) die de scheiding duidelijker maken.

4. Test opnieuw

Herhaal de incognitotest: de aanvraag zonder sessie moet een lege lijst of een 401-fout teruggeven. Log daarna in met twee verschillende accounts en bevestig dat geen van beiden de data van de ander ziet. Heb je geautomatiseerde tests, dan is dit het geval dat er een verdient.

Veelgemaakte fouten bij het oplossen

  • Policies met using (true). AI-assistenten stellen ze vaak voor "zodat het werkt". Ze laten de tabel net zo open als helemaal geen RLS.
  • with check vergeten. Dat beschermt lezen, maar laat schrijven namens iemand anders toe.
  • "Catalogus"-tabellen met persoonsgegevens. Een publieke profielentabel die namen moet tonen, stelt uiteindelijk e-mailadressen of telefoonnummers bloot. Stel alleen de benodigde kolommen bloot via een view.
  • Views die RLS omzeilen. In Postgres draait een view met de rechten van wie hem aanmaakte. Aangemaakt vanuit het dashboard kan hij rijen teruggeven die de policies van de basistabel zouden verbieden. Maak views aan met security_invoker = true (Postgres 15 of nieuwer) of loop ze één voor één na.
  • Storage zonder policies. Buckets hebben hun eigen regels. Een publieke bucket serveert elk bestand aan iedereen met de URL; privédocumenten horen in privébuckets met policies per gebruiker.
  • Alleen productie repareren. Als je een ontwikkelproject met gekopieerde data hebt, ligt dat net zo bloot.

Als je al data hebt blootgesteld

Sluit eerst de deur (de stappen hierboven). Roteer daarna elke key die in leesbare tabellen stond: API-keys van derden, tokens, webhooks. Bekijk de API-logs in het Supabase-dashboard om in te schatten wat er is gelezen en sinds wanneer. Bevat de data persoonsgegevens van Europese gebruikers, dan verplicht de AVG je om te beoordelen of de toezichthouder binnen 72 uur moet worden geïnformeerd en, bij hoog risico, de gebruikers zelf. Documenteer wat je hebt gevonden en wat je hebt gedaan: daar wordt om gevraagd als er iemand aanklopt.

Een prompt om het met je AI op te lossen

Werk je met Cursor, Claude of de chat van Lovable zelf, dan geeft deze prompt meestal een goed resultaat: "Som alle tabellen op in het schema public van mijn Supabase-project. Zet voor elke tabel Row-Level Security aan en schrijf aparte policies voor select, insert, update en delete, beperkt tot de rol authenticated, zodat elke gebruiker alleen rijen benadert waarvan de kolom user_id gelijk is aan (select auth.uid()). Gebruik with check bij insert en update. Vertel me welke tabellen geen eigenaarskolom hebben, zodat ik die met de hand kan beslissen. Gebruik nooit using (true)."

Hoe Scanaris dit controleert

Scanaris scant je site read-only en herkent de vingerafdruk van dit patroon: Supabase-endpoints die vanuit de browser bereikbaar zijn, te ruime CORS, keys en tokens die in je JavaScript blootliggen, en data die de app zonder sessie serveert. Geef je een read-only connection string, dan auditeert het ook tabel voor tabel of RLS aanstaat en of er policies zijn. Plak je URL en het vertelt je in seconden, gerangschikt op ernst, wat in het zicht ligt, voordat iemand anders het vindt.

Veelgestelde vragen

Heeft Lovable dit al opgelost?

Lovable heeft veranderd hoe het tabellen genereert en waarschuwingen toegevoegd, maar een app die vóór 15 april 2025 is gepubliceerd blijft zoals jij hem achterliet: policies voegen zichzelf niet toe. En in elk project, nieuw of oud, heeft elke tabel die je daarna aanmaakt zijn eigen policies nodig.

Breekt RLS aanzetten mijn app?

Aanzetten zonder policies wel: alles wordt geweigerd. Dat is het juiste gedrag. Schrijf de policies uit stap 2 en de app werkt weer, maar alleen voor de mensen voor wie hij bedoeld is.

Kan ik de tabel vanuit de frontend beschermen?

Nee. De frontend is code die op de computer van de bezoeker draait en naar believen kan worden aangepast. Het enige dat niet te omzeilen is, zijn de regels die de database afdwingt.

Wat als mijn app geen gebruikers heeft, alleen een publieke inhoudstabel?

Dan is een publieke leespolicy legitiem: for select to anon, authenticated using (true). Wat er niet mag zijn, is een open schrijf-policy of kolommen met persoonsgegevens in die tabel.

Hoe staat je site ervoor?

Laat Scanaris er even overheen gaan en check het in 30 seconden, gratis.

Mijn site scannen

Gerelateerde checks

Beveiligingsgidsen per stack