// docs / baas security / supabase rls scanner
Supabase RLS-scanner: vind tabellen met ontbrekende of kapotte row-level security
Row-level security (RLS) is het enige dat tussen de gegevens van je klanten en het internet staat wanneer je een Supabase-gebaseerde app lanceert. AI-codeertools genereren RLS-vormige code die compileert, wordt gelanceerd en stilletjes gegevens lekt β tabellen die zonder RLS-activatie zijn aangemaakt, beleidsregels die lezen maar nooit beperken, predicaten die een kolom met zichzelf vergelijken. Dit artikel laat zien wat een Supabase RLS-scanner van buitenaf kan bewijzen, de vier kapotte-RLS-vormen die opduiken in vibe-gecodeerde apps, en hoe je je eigen deployment in minder dan een minuut kunt scannen.
Wat een externe RLS-scan kan bewijzen
Een passieve RLS-scan loopt tegen het PostgREST-endpoint dat Supabase blootstelt op https://[project].supabase.co/rest/v1/. Het gebruikt alleen de publiceerbare anon-sleutel β dezelfde sleutel die je browser gebruikt β en sondeert op tabellijst-metadata, anonieme reads en anonieme writes. Het authenticeert nooit als een gebruiker en raakt nooit service-rolprivileges aan. Alles wat het kan doen, kan een niet-geauthenticeerde aanvaller op het internet ook doen.
Van buiten de database kan een scanner het volgende met hoog vertrouwen bevestigen:
- RLS is uitgeschakeld op een tabel. PostgREST retourneert rijen voor een anonieme
SELECTwanneer RLS uitstaat of wanneer een beleid het toestaat. Beide gevallen zijn een bevinding. - De anonieme rol kan tabellen oplijsten. Een
GET /rest/v1/met de anon-sleutel retourneert het OpenAPI-schema voor elke tabel waarop deanon-rol enig privilege heeft. AI-gegenereerde apps verlenen vaakUSAGEop het schema enSELECTop elke tabel, wat de volledige schema-kaart blootstelt, zelfs wanneer RLS de feitelijke reads weigert. - De anonieme rol kan invoegen. Een sonderende
POSTmet een gok naar de kolomvorm slaagt als RLS geenINSERT-beleid heeft dat het weigert β zelfs alsSELECTvergrendeld is. - De service-rolsleutel staat in de browserbundle. Aangrenzend aan RLS: als een scanner
SUPABASE_SERVICE_ROLE_KEYof een JWT metrole: service_rolein de JavaScript-bundle vindt, is RLS irrelevant β de houder van die sleutel omzeilt elk beleid.
Wat een externe scan niet kan bewijzen
Wees eerlijk over de grenzen van de scanner. Een externe RLS-scan kan je pg_policies-tabel, je migratiebestanden of het exacte predicaat van een beleid niet lezen. Het maakt gevolgtrekkingen uit black-box-gedrag, wat betekent dat het soms een bevinding rapporteert die opzettelijk publieke gegevens blijkt te zijn (een marketingnieuwsbrieftabel, een openbare productcatalogus). Het FixVibe-rapport markeert deze als gemiddeld vertrouwen wanneer de scanner de intentie niet kan onderscheiden β bekijk de tabelnaam en beslis.
De vier kapotte-RLS-vormen die AI-tools produceren
Wanneer je Cursor, Claude Code, Lovable of Bolt op Supabase richt, ontstaan dezelfde vier kapotte-RLS-patronen in duizenden apps. Elk slaagt voor type-check, compileert en wordt gelanceerd:
Vorm 1: RLS is nooit ingeschakeld
De meest voorkomende faalmodus. De migratie maakt de tabel aan, maar de ontwikkelaar (of de AI-tool) vergeet ALTER TABLE ... ENABLE ROW LEVEL SECURITY. PostgREST serveert vrolijk de hele tabel aan iedereen met de anon-sleutel. Fix: ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY; ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;. FORCE is niet optioneel β zonder dit omzeilt de tabeleigenaar (en elke rol met tabeleigenaarschap) RLS.
ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;Vorm 2: RLS ingeschakeld, geen beleidsregels
Een subtielere mislukking. RLS is ingeschakeld, maar er zijn geen beleidsregels geschreven. De standaard in PostgreSQL is weigeren, dus geauthenticeerde gebruikers zien niets β en de ontwikkelaar voegt USING (true) toe om de app werkend te krijgen, wat iedereen toestaat alles te lezen. Fix: schrijf een beleid dat scoopt op auth.uid(): CREATE POLICY "select_own" ON public.[name] FOR SELECT USING (auth.uid() = user_id); en een bijpassend INSERT/UPDATE/DELETE-beleid.
CREATE POLICY "select_own"
ON public.[name]
FOR SELECT
USING (auth.uid() = user_id);Vorm 3: Beleid vergelijkt kolom met zichzelf
A copy-paste artefact. The developer writes <code>USING (user_id = user_id)</code> β which is always true β instead of <code>USING (auth.uid() = user_id)</code>. Type-checks pass; the policy permits every row. <strong>Fix:</strong> always compare a column to a function call (<code>auth.uid()</code>, <code>auth.jwt()->>'org_id'</code>, etc.), never to itself or to a constant.
Vorm 4: Beleid op SELECT maar niet op INSERT/UPDATE
De ontwikkelaar vergrendelt reads maar vergeet writes. RLS-beleidsregels zijn per-opdracht. FOR SELECT beschermt alleen reads; een anonieme client kan nog steeds INSERTen als geen beleid het weigert. Fix: schrijf een beleid per opdracht of gebruik FOR ALL met expliciete USING- en WITH CHECK-clausules.
Hoe de FixVibe Supabase RLS-scanner werkt
De baas.supabase-rls-check loopt in drie fasen, elk met expliciete vertrouwensniveaus:
- Fase 1 β fingerprinten. De scanner crawlt de gedeployde app, parseert de JavaScript-bundle en extraheert de Supabase-project-URL en anon-sleutel uit de runtimeconfiguratie. Geen DNS-gokken, geen brute force β het leest wat de browser leest.
- Fase 2 β schema-ontdekking. Een enkele
GET /rest/v1/met de anon-sleutel retourneert het OpenAPI-schema voor elke tabel die de anon-rol kan zien. De scanner registreert tabelnamen maar leest in dit stadium geen rijgegevens. - Fase 3 β lees- en schrijfsondes. Voor elke ontdekte tabel geeft de scanner één anonieme
SELECTuit metlimit=1. Als rijen worden geretourneerd, is RLS toegestaan. De scanner stopt daar β hij somt geen rijen op, pagineert niet, wijzigt geen gegevens. INSERT-sondes zijn afgeschermd achter geverifieerd domeineigenaarschap en expliciete opt-in; ze worden nooit afgevuurd tegen ongeverifieerde doelen.
Elke bevinding wordt geleverd met de exacte aanvraag-URL, antwoordstatus, antwoordvorm (alleen header) en de tabelnaam. De AI-fixprompt onderaan de bevinding is een copy-paste SQL-blok dat je uitvoert in de Supabase SQL-editor.
Wat te doen wanneer de scanner iets vindt
Elke RLS-bevinding is een runtime-noodsituatie. Openbare PostgREST-endpoints worden binnen enkele minuten door aanvallers gescand. De herstelsequentie is mechanisch:
- Audit elke tabel. Voer
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';uit in de Supabase SQL-editor. Elke rij metrowsecurity = falseis een probleem. - Schakel RLS in op elke openbare tabel. Maak
ENABLE ROW LEVEL SECURITYenFORCE ROW LEVEL SECURITYstandaard op elke aangemaakte tabel β maak er een migratiesjabloon van. - Schrijf beleidsregels per opdracht. Gebruik geen
FOR ALL USING (true). Schrijf expliciete beleidsregels voor SELECT, INSERT, UPDATE, DELETE β elk gescoopt opauth.uid()of een org-id-kolom uitauth.jwt(). - Verifieer met een tweede account. Meld je aan als een andere gebruiker, probeer de records van een andere gebruiker rechtstreeks via de REST API te lezen. Als het antwoord
200is, is het beleid kapot. - Hersannen. Voer na het toepassen van de fix opnieuw een FixVibe-scan uit tegen dezelfde URL. De
baas.supabase-rls-bevinding zou moeten verdwijnen.
-- Audit every table for missing RLS. Run in the Supabase SQL editor.
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;Hoe dit zich verhoudt tot andere scanners
De meeste generieke DAST-tools (Burp Suite, OWASP ZAP, Nessus) weten niet wat PostgREST is. Ze crawlen je app, negeren het /rest/v1/-pad en rapporteren over de HTML-pagina's die ze wel begrijpen. Snyk en Semgrep zijn statische-analysetools β ze vinden migratiebestanden in je repo met ontbrekende RLS-aanroepen, maar kunnen niet bewijzen dat de gedeployde database verkeerd is geconfigureerd. FixVibe zit in de kloof: passief, BaaS-bewust, gericht op wat een niet-geauthenticeerde aanvaller vanaf de openbare URL kan bewijzen.
Veelgestelde vragen
Leest of wijzigt de scanner mijn gegevens?
Nee. Passieve scans geven hooguit één SELECT ... limit=1 uit per ontdekte tabel om te bevestigen of RLS anonieme reads toestaat. De scanner registreert de antwoordvorm, niet de rij-inhoud. INSERT-, UPDATE- en DELETE-sondes zijn afgeschermd achter geverifieerd domeineigenaarschap en lopen nooit tegen ongeverifieerde doelen.
Werkt dit als mijn Supabase-project gepauzeerd is of op een aangepast domein staat?
Gepauzeerde projecten retourneren 503 op elke aanvraag β de scanner rapporteert het project als onbereikbaar. Aangepaste domeinen werken zolang de gedeployde app nog steeds de Supabase-client-SDK in de browser laadt; de scanner extraheert de project-URL hoe dan ook uit de bundle.
Wat als mijn anon-sleutel wordt geroteerd of mijn publiceerbare sleutel verandert?
Voer de scan opnieuw uit. De scanner extraheert de sleutel bij elke run opnieuw uit de huidige bundle. Rotatie maakt alleen het vorige rapport ongeldig, niet de beleidsstatus van de database.
Controleert de scanner het nieuwe Supabase-publiceerbare-sleutelmodel (<code>sb_publishable_*</code>)?
Ja. De detector herkent zowel legacy anon-JWT's als de nieuwere sb_publishable_*-sleutels en behandelt ze identiek β beide zijn bedoeld om openbaar te zijn en beide laten RLS over als de enige verdedigingslinie.
Volgende stappen
Voer een gratis FixVibe-scan uit tegen je productie-URL β de baas.supabase-rls-check is ingeschakeld op elk plan, inclusief de gratis tier. Voor een diepere blik op wat er nog kan lekken uit een Supabase-project, zie Supabase service-rolsleutel blootgesteld in JavaScript en Supabase opslagbucket-beveiligingschecklist. Voor het paraplu-overzicht over alle BaaS-providers, lees BaaS-misconfiguratiescanner.
