Naar de inhoud
Datadenkt

Gereedschap  /  Beveiliging

Je database staat open en je app ziet er prima uit

Row Level Security: waarom je Supabase-tabel voor iedereen leesbaar is en hoe je dat in vier regels SQL dichtzet.

Bij Supabase praat de browser rechtstreeks met de database in plaats van via jouw server. Dat is geen fout maar het ontwerp, en het is precies waarom je in één avond iets werkends hebt. Het gevolg is wel dat de sleutel die daarvoor nodig is, de anon key, gewoon in je JavaScript staat. Die sleutel houdt op zichzelf niets tegen. Wat wél tegenhoudt is Row Level Security: regels per rij die bepalen wie wat mag zien. En op een nieuwe tabel staat RLS standaard uit.

Meenemen naar je editor

Deze handleiding is geschreven om als werkopdracht te dienen. Neem hem in zijn geheel mee naar Cursor, Claude Code of waar je ook bouwt, en werk hem fase voor fase af.

Waar het misgaat

Je merkt er niets vanJe app werkt, je ziet je eigen gegevens, en ondertussen kan iedereen de hele tabel lezen.
Een leesverzoek laat geen spoor naJe kunt achteraf niet zien wie er langs is geweest. Een tabel die openstond behandel je als gelekt.
RLS aanzetten zonder regels legt je app platZonder policies is de tabel voor iedereen dicht, ook voor jezelf. Eerst dicht, dan gericht open.
De service role key gaat overal langsDie sleutel negeert alle regels. Hij hoort alleen op de server en nooit in je repository.

Fase 0Vaststellen

Twintig seconden werk, en daarna weet je of dit jouw probleem is.

  1. Open je Supabase-dashboard, ga naar Advisors en dan naar Security Advisor. Elke tabel zonder RLS staat daar met een rode melding. Dit is de snelste controle die er is.
  2. Wil je het met eigen ogen zien: open je site, druk op F12, ga naar het tabblad Network en herlaad de pagina. Zoek een verzoek naar jouw-project.supabase.co; daar staat de anon key in.
  3. Open een privévenster en doe met die sleutel een verzoek naar een tabel waar je niets te zoeken hebt. Krijg je rijen van andere gebruikers terug, dan is dit jouw probleem.
  4. Doe dit uitsluitend op je eigen project. Op andermans database is dit geen controle maar een inbraak.

Fase 1De deur dicht

RLS aanzetten per tabel. Hierna kan niemand meer iets, inclusief je eigen app. Dat is de bedoeling.

Doe dit tabel voor tabel en niet in één keer voor je hele database. Je wilt per tabel kunnen zien wat er stukgaat, want dat vertelt je precies welke regels je nog moet schrijven.

RLS aanzetten
alter table public.projecten enable row level security;

Fase 2Gericht openzetten

Vier regels voor het normale geval: een tabel waarin elke rij bij één gebruiker hoort.

De vier standaardregels
-- Elke rij hoort bij één gebruiker via de kolom gebruiker_id.

create policy "eigen rijen lezen"
  on public.projecten for select
  using (auth.uid() = gebruiker_id);

create policy "zelf aanmaken"
  on public.projecten for insert
  with check (auth.uid() = gebruiker_id);

create policy "eigen rijen bijwerken"
  on public.projecten for update
  using (auth.uid() = gebruiker_id)
  with check (auth.uid() = gebruiker_id);

create policy "eigen rijen verwijderen"
  on public.projecten for delete
  using (auth.uid() = gebruiker_id);

Het verschil tussen using en with check

Dit is het stuk dat bijna iedereen overslaat en waar het daarna alsnog misgaat. `using` bepaalt welke rijen je mag zien of aanraken. `with check` bepaalt hoe een rij eruit mag zien nadat je hem hebt geschreven.

Laat je bij update de `with check` weg, dan mag een gebruiker zijn eigen rij pakken en er een ander gebruiker_id in zetten. Hij geeft zijn rij dan weg aan iemand anders, of schuift hem naar een account dat hij zelf ook beheert. Zet ze allebei.

Fase 3De drie dingen die RLS niet oplost

Zodat je niet denkt dat je klaar bent terwijl er nog een deur openstaat.

WatWaarom RLS niet helptWat wel
De service role keyDie sleutel gaat langs alle regels heen, dat is zijn doelNooit in de browser, alleen op de server, en nooit in je repository
Storage-bucketsBestanden hebben eigen regels, los van je tabellenZet de bucket op private en schrijf er apart beleid voor
Je eigen API-routesDie draaien op de server en zijn vaak precies de plek waar je de regels omzeiltControleer daar zelf wie de aanvrager is, elke route opnieuw

Fase 4Controleren

Bewijs voordat je zegt dat het klaar is. Niet omdat het hoort, maar omdat je dit lek niet aan je app kunt zien.

  • Security Advisor staat schoonGeen enkele tabel meldt nog dat RLS uit staat. Dit is de meting, niet je gevoel.
  • Je eigen app werkt nogKlik door alles waar je gegevens laadt of opslaat. Een fout van het type new row violates row-level security policy betekent dat je insert-regel ontbreekt of niet klopt.
  • Een tweede account ziet de eerste nietMaak een testgebruiker aan, log in, en controleer dat je alleen je eigen rijen ziet. Dit is de enige controle die het echte gedrag bewijst.
  • Update kan de eigenaar niet veranderenProbeer met de testgebruiker een rij bij te werken en er een ander gebruiker_id in te zetten. Dat moet weigeren. Lukt het wel, dan mist je with check.
  • De service role key staat niet in je frontendZoek in je hele project op service_role. Elke treffer buiten je servercode is een probleem.
Let op

Stond een tabel al langer open, dan is dichtzetten niet genoeg. Je weet niet wie er langs is geweest, dus behandel de gegevens als gelekt en beoordeel of je gebruikers moet informeren.

Nog een keer, nu je weet wat erin staat

Alles hierboven in één keer, inclusief de fases, de SQL en de controlestap.

Meer uit beveiliging

Werkt dit, maar wil je het niet zelf doen?

Dit blad is gratis en blijft gratis. Wil je dat het bij jou automatisch draait in plaats van met de hand, dan bouw ik dat. Beschrijf in vijf vragen wat er vastloopt en stuur het adres van je app mee; binnen één werkdag heb je mijn antwoord op papier.

Vertel wat er vastloopt