Kort sagt
En policy som auth_in_org(organization_id) frågar “är den som anropar medlem i organisationen som äger raden?”. För en användare som tillhör organisation A och organisation B är svaret ja i båda. I min sond gav enbart RLS en sådan användare läs- och skrivåtkomst till ett dokument i den andra organisationen. När jag lade till organisationen för den aktuella förfrågan i frågan fick jag 0 rader.
Jag hittade det här när jag härdade plattformen för byggdokument, en multi-tenant-app på Supabase. Varje tenant-tabell hade row level security aktiverat och tvingat, så det såg färdigt ut.
Vad såg säkert ut?
Varje policy hade samma form: radens organization_id måste vara en organisation som den som anropar är medlem i. Därför slog några server actions upp ett dokument enbart på id. Resonemanget var “RLS filtrerar det ändå”.
const { data } = await supabase
.from('documents')
.select('*')
.eq('document_id', id)
.single()
För en användare med ett enda medlemskap fungerar det, och ett test med en sådan användare går igenom.
Varför släpper RLS igenom den andra organisationen?
Policyn är ett medlemskapstest. Postgres utvärderar den för varje rad, som dokumentationen om row security beskriver, och den vet inte vilken organisation förfrågan agerar som, eftersom det bor i appen: i URL:en, en cookie eller organisationsväxlaren. För en användare som är medlem i båda organisationerna passerar varje rad i båda.
Filtret på organization_id i appen är alltså inte ett andra lager ovanpå RLS. För den här användaren är det den enda kontrollen.
När du ändå är inne i de hjälpfunktionerna: kontrollera om de tittar på organizations.is_active. Medlemskapshjälpare gör ofta inte det, och då behåller en avaktiverad organisation läs- och skrivåtkomst i RLS-lagret. När den enda is_active-kontrollen ligger i appens tenant-resolver hoppar varje fråga som går förbi resolvern också över avaktiveringen.
Vad åtgärdade det?
Organisationen för förfrågan blev ett obligatoriskt argument i varje tjänstefunktion som läser eller skriver tenant-data. Ett anropsställe kan inte glömma den, eftersom funktionen inte kompilerar utan den.
export async function getDocument(orgId: string, id: string) {
return supabase
.from('documents')
.select('*')
.eq('organization_id', orgId)
.eq('document_id', id)
.single()
}
Två regler till kom ut av granskningen:
- Ta aldrig organisationen från raden du ska skriva.
update ... where id = $1 and organization_id = <that row's own organization_id>är alltid sant, så det skyddar ingenting. - En sammansatt främmande nyckel på
(id, organization_id)gör att databasen vägrar en koppling mellan rader från olika organisationer. Kontrollera varje barntabell: det är lätt att lägga den på tre och missa den fjärde.
Hur kontrollerar du din egen app?
Skapa en användare med ett aktivt medlemskap i två organisationer. Agera sedan som den användaren i SQL-editorn på en lokal kopia eller en staging-kopia och be om en rad från den andra organisationen:
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;
Upprepa med en update, och en gång till efter att du satt den andra organisationen som inaktiv. Om den första frågan returnerar en rad: titta på varje ställe som frågar tenant-data enbart på id.
Hur illa är det?
Bedöm omfattningen ärligt. Angriparen behöver ett verkligt medlemskap i båda organisationerna, så det här handlar om integritet och granskningsspår: någon med åtkomst till två kunder ändrar i fel kund, och loggen säger att det var tillåtet. Det är inte ett sätt för en främling att läsa alla tenants.
Den allmänna lärdomen är den från hur jag testar Supabase RLS: testa vad en verklig användare kan göra, med verkliga dataformer, inte bara vad varje policy säger. Tre närliggande anteckningar: en roll hämtad från registreringsmetadata, en NULL som hoppade över en behörighetskontroll och vilka scheman Data API:et serverar. Tvåföretagstestet är den första punkten i min säkerhetschecklista för Lovable-appar. För policyer, behörigheter och inställningar runt omkring kör den gratis Supabase säkerhetskontroll 46 OWASP Top 10:2025-regler; om en policy släpper igenom en användare i två organisationer är en fråga om dataform som bara frågan ovan besvarar.