---
title: "Supabase RLS: användare i två organisationer läcker"
description: "En RLS-policy som kontrollerar medlemskap är sann i båda organisationerna för en användare i två. Min sond, åtgärden och en kontroll på två minuter."
url: "https://madsalt.tech/sv/notes/supabase-rls-user-in-two-organizations/"
author: "Denys Zaichenko"
language: "sv"
published: 2026-10-18
---

# Supabase RLS: användare i två organisationer läcker

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](https://madsalt.tech/sv/work/construction-document-platform/), 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å".

```ts
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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) 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.

![Vilken org en förfrågan tillhör står inte i policyn. En användare, medlem i org A och org B, arbetar i org A. Enbart RLS-policy: Anrop i org A → Medlem i radens org? → Ja, A eller B → Rad från org B; RLS plus förfrågans org: Anrop i org A → Medlem i radens org? → Rad i org A? → 0 rader från org B.](https://madsalt.tech/charts/sv/supabase-rls-user-in-two-organizations.svg)

*Min sond på en seedad användare i två orgar, september 2026: enbart RLS gav läs- och skrivåtkomst till dokumentet i org B; med förfrågans org blev det 0 rader.*

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.

```ts
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:

```sql
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](https://madsalt.tech/sv/blog/how-to-test-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](https://madsalt.tech/sv/notes/supabase-raw-user-meta-data-role/), [en NULL som hoppade över en behörighetskontroll](https://madsalt.tech/sv/notes/plpgsql-if-not-null-fails-open/) och [vilka scheman Data API:et serverar](https://madsalt.tech/sv/notes/supabase-exposed-schemas/). Tvåföretagstestet är den första punkten i min [säkerhetschecklista för Lovable-appar](https://madsalt.tech/sv/blog/lovable-app-security-checklist/). För policyer, behörigheter och inställningar runt omkring kör den gratis [Supabase säkerhetskontroll](https://madsalt.tech/sv/tools/supabase-security-check/) 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.
