---
title: "Supabase RLS: bruker i to organisasjoner lekker data"
description: "En RLS-policy som sjekker medlemskap er sann i begge organisasjonene for en bruker i to. Hva proben min fant, løsningen og en sjekk på to minutter."
url: "https://madsalt.tech/no/notes/supabase-rls-user-in-two-organizations/"
author: "Denys Zaichenko"
language: "no"
published: 2026-10-18
---

# Supabase RLS: bruker i to organisasjoner lekker data

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

```ts
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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) 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.](https://madsalt.tech/charts/no/supabase-rls-user-in-two-organizations.svg)

*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.

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

```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;
```

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](https://madsalt.tech/no/blog/how-to-test-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](https://madsalt.tech/no/notes/supabase-raw-user-meta-data-role/), [en NULL som hoppet over en tilgangssjekk](https://madsalt.tech/no/notes/plpgsql-if-not-null-fails-open/) og [hvilke skjemaer Data API-et serverer](https://madsalt.tech/no/notes/supabase-exposed-schemas/). To-selskaper-testen er det første punktet i [sikkerhetsjekklisten min for Lovable-apper](https://madsalt.tech/no/blog/lovable-app-security-checklist/). For policyer, tilganger og innstillinger rundt kjører den gratis [Supabase sikkerhetssjekk](https://madsalt.tech/no/tools/supabase-security-check/) 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.
