Hopp til innhold

Supabase RLS: bruker i to organisasjoner lekker data

Supabase

Vis som MarkdownÅpne i ClaudeÅpne i ChatGPTÅpne i Perplexity

Spørsmålet kopieres også. Hvis chatten åpnes tom, lim det inn.

TL;DR

En policy som auth_in_org(organization_id) spør “er den som kaller medlem av organisasjonen som eier raden?”. For en bruker som tilhører organisasjon A og organisasjon B er svaret ja i begge. I proben min ga RLS alene en slik bruker lese- og skrivetilgang til et dokument i den andre organisasjonen. Når jeg la til organisasjonen til den aktuelle forespørselen i spørringen, fikk jeg 0 rader.

Jeg fant dette mens jeg herdet dokumentplattformen for bygg, en multi-tenant-app på Supabase. Hver tenant-tabell hadde row level security slått på og tvunget, så det så ferdig ut.

Hva så trygt ut?

Hver policy hadde samme form: radens organization_id må være en organisasjon den som kaller er medlem av. Derfor slo noen server actions opp et dokument bare på id. Tankegangen var “RLS filtrerer det uansett”.

const { data } = await supabase
  .from('documents')
  .select('*')
  .eq('document_id', id)
  .single()

For en bruker med ett medlemskap fungerer dette, og en test med en slik bruker går gjennom.

Hvorfor slipper RLS gjennom den andre organisasjonen?

Policyen er en medlemskapstest. Postgres evaluerer den for hver rad, slik dokumentasjonen om row security beskriver, og den vet ikke hvilken organisasjon forespørselen opptrer som, fordi det bor i appen: i URL-en, en cookie eller organisasjonsbryteren. For en bruker som er medlem av begge organisasjonene, passerer alle rader i begge.

Hvilken org en forespørsel tilhører står ikke i policyen. Én bruker, medlem av org A og org B, jobber i org A. Bare RLS-policy: Kall i org A → Medlem av radens org? → Ja, A eller B → Rad fra org B vist; RLS pluss forespørselens org: Kall i org A → Medlem av radens org? → Rad i org A? → 0 rader fra org B.
Proben min på en seedet bruker i to orger, september 2026: RLS alene ga lese- og skrivetilgang til dokumentet i org B; med forespørselens org ble det 0 rader.

Filteret på organization_id i appen er altså ikke et ekstra lag oppå RLS. For denne brukeren er det eneste kontroll.

Når du først er inne i de hjelpefunksjonene: sjekk om de ser på organizations.is_active. Medlemskapshjelpere gjør ofte ikke det, og da beholder en deaktivert organisasjon lese- og skrivetilgang i RLS-laget. Når den eneste is_active-sjekken ligger i appens tenant-resolver, hopper hver spørring som går utenom resolveren også over deaktiveringen.

Hva løste det?

Organisasjonen til forespørselen ble et påkrevd argument i hver tjenestefunksjon som leser eller skriver tenant-data. Et kallsted kan ikke glemme den, fordi funksjonen ikke kompilerer uten.

export async function getDocument(orgId: string, id: string) {
  return supabase
    .from('documents')
    .select('*')
    .eq('organization_id', orgId)
    .eq('document_id', id)
    .single()
}

To regler til kom ut av gjennomgangen:

  • Aldri ta organisasjonen fra raden du skal skrive. update ... where id = $1 and organization_id = <that row's own organization_id> er alltid sant, så det beskytter ingenting.
  • En sammensatt fremmednøkkel på (id, organization_id) gjør at databasen nekter en kobling mellom rader fra ulike organisasjoner. Sjekk hver barnetabell: det er lett å legge den på tre og glemme den fjerde.

Hvordan sjekker du din egen app?

Opprett én bruker med et aktivt medlemskap i to organisasjoner. Opptre så som den brukeren i SQL-editoren på en lokal kopi eller en staging-kopi, og be om en rad fra den andre organisasjonen:

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub": "<user id>", "role": "authenticated"}';

-- RLS alone: returns the org B document
select document_id from documents where document_id = '<org B document id>';

-- with the org of the request: 0 rows
select document_id from documents
where document_id = '<org B document id>'
  and organization_id = '<org A id>';
rollback;

Gjenta med en update, og én gang til etter at du har satt den andre organisasjonen som inaktiv. Hvis den første spørringen returnerer en rad: se på hvert sted som spør etter tenant-data bare på id.

Hvor ille er det?

Vurder omfanget ærlig. Angriperen trenger et ekte medlemskap i begge organisasjonene, så dette handler om integritet og revisjonsspor: noen med tilgang til to kunder endrer feil kunde, og loggen sier at det var tillatt. Det er ikke en måte for en fremmed å lese alle tenants på.

Den generelle lærdommen er den fra hvordan jeg tester Supabase RLS: test hva en ekte bruker kan gjøre, med ekte dataformer, ikke bare hva hver policy sier. Tre beslektede notater: en rolle hentet fra registreringsmetadata, en NULL som hoppet over en tilgangssjekk og hvilke skjemaer Data API-et serverer. To-selskaper-testen er det første punktet i sikkerhetsjekklisten min for Lovable-apper. For policyer, tilganger og innstillinger rundt kjører den gratis Supabase sikkerhetssjekk 46 OWASP Top 10:2025-regler; om en policy slipper gjennom en bruker i to organisasjoner, er et spørsmål om dataform som bare spørringen ovenfor besvarer.

© 2026 Denys Zaichenko. Opprinnelig artikkel: https://madsalt.tech/no/notes/supabase-rls-user-in-two-organizations/