Security Academy→Course→Database Security
Modul 5 / 12

Database Security

Postgres, Supabase og RLS: når anon-nøkkelen ligger i browseren er policies din siste forsvarslinje.

Modellen

I Supabase-lignende arkitektur snakker browseren direkte med databasen via en publishable/anon key. Den nøkkelen er offentlig. Det eneste som skiller brukerne er Row Level Security.

service_role bypasser RLS. Den skal kun brukes på server, og kun etter at du har verifisert kalleren.

  • ›Er RLS aktivert på ALLE tabeller i public?
  • ›Har anon-rollen grants/policies den ikke trenger?
  • ›Er policies like strenge for INSERT/UPDATE/DELETE som for SELECT?
  • ›Har UPDATE både USING og WITH CHECK?
  • ›Tenant isolation: sjekkes org_id via medlemskap, ikke fra klientverdi?

Feil policy: alle innloggede kan lese alt

AI genererer ofte «using (true)» for å få appen til å fungere. Det gir alle autentiserte brukere tilgang til alle rader.

Vulnerable · sql
alter table documents enable row level security;

create policy "read" on documents
  for select to authenticated using (true);

create policy "update" on documents
  for update to authenticated using (owner_id = auth.uid());
  -- mangler WITH CHECK: eier kan sette owner_id til en annen
Secure · sql
create policy "owner read" on documents
  for select to authenticated
  using (owner_id = auth.uid());

create policy "owner update" on documents
  for update to authenticated
  using (owner_id = auth.uid())
  with check (owner_id = auth.uid());

-- Tenant: medlemskap sjekkes i egen tabell
create policy "org members read" on org_docs
  for select to authenticated
  using (public.is_org_member(auth.uid(), org_id));

Test RLS direkte fra browseren

Logg inn som User B og kjør i Console (klienten er allerede lastet i appen eller kan opprettes med publishable key):

// Forventet: [] eller bare egne rader
const { data } = await supabase.from("documents").select("*");
console.table(data);

// Forventet: 0 rader endret / feil
await supabase.from("documents").update({ title: "x" }).eq("id", "<A sin id>");

// Anonym test: uten innlogging
await supabase.auth.signOut();
await supabase.from("profiles").select("email");

Rekursjon og roller

Policies som spør samme tabell (f.eks. profiles.role) gir rekursjon og ofte en «midlertidig» using(true). Bruk en SECURITY DEFINER-funksjon som has_role() mot en egen user_roles-tabell.

Kontroller i din egen app
Labs for denne modulen

Quiz

Scenario 1

Tabellen messages har RLS aktivert og én policy: for select using (sender_id = auth.uid() or recipient_id = auth.uid()). Kan en bruker sette inn meldinger som en annen avsender?

Scenario 2

En server function bruker supabaseAdmin for å hente brukerens ordre fordi «RLS ga tomt resultat». Hva er riktig respons?