---
title: "Supabase multi-tenant RLS: únik dát u používateľa v 2 org"
description: "Politika RLS kontrolujúca členstvo platí v oboch organizáciách pre používateľa, ktorý patrí do dvoch. Čo našla sonda, oprava, kontrola na dve minúty."
url: "https://madsalt.tech/sk/notes/supabase-rls-user-in-two-organizations/"
author: "Denys Zaichenko"
language: "sk"
published: 2026-10-18
---

# Supabase multi-tenant RLS: únik dát u používateľa v 2 org

TL;DR

Politika typu `auth_in_org(organization_id)` sa pýta „je volajúci členom organizácie, ktorej riadok patrí?“. Pre používateľa z organizácie A aj B je odpoveď v oboch prípadoch áno. V mojej sonde malo samotné RLS takému používateľovi čítanie aj zápis k dokumentu druhej organizácie. Po pridaní organizácie aktuálnej požiadavky do dotazu sa vrátilo 0 riadkov.

Narazil som na to pri spevňovaní [platformy pre stavebnú dokumentáciu](https://madsalt.tech/sk/work/construction-document-platform/), multi-tenant aplikácie na Supabase. Každá tenantná tabuľka mala zapnuté a vynútené row level security, takže to vyzeralo hotovo.

## Čo vyzeralo bezpečne?

Každá politika mala rovnaký tvar: `organization_id` riadku musí byť organizácia, ktorej členom volajúci je. Preto niekoľko serverových akcií vyhľadávalo dokument iba podľa id. Úvaha znela „RLS to aj tak vyfiltruje“.

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

Pri používateľovi s jedným členstvom to funguje a test s takým používateľom prejde.

## Prečo RLS pustí druhú organizáciu?

Politika je test členstva. Postgres ju vyhodnocuje pre každý riadok, ako opisuje [dokumentácia row security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html), a nevie, za ktorú organizáciu požiadavka koná, pretože to žije v aplikácii: v URL, cookie alebo prepínači organizácií. Pri používateľovi, ktorý je členom oboch organizácií, prejdú všetky riadky oboch.

![Do ktorej org požiadavka patrí, v politike nie je. Jeden používateľ, člen org A aj org B, pracuje v org A. Samotná politika RLS: Požiadavka v org A → Člen org riadku? → Áno, A aj B → Vrátený riadok org B; RLS plus org tejto požiadavky: Požiadavka v org A → Člen org riadku? → Riadok v org A? → 0 riadkov z org B.](https://madsalt.tech/charts/sk/supabase-rls-user-in-two-organizations.svg)

*Moja sonda na založenom používateľovi v dvoch org, september 2026: samotné RLS dalo čítanie aj zápis k dokumentu org B; po pridaní org požiadavky sa vrátilo 0 riadkov.*

Filter `organization_id` v aplikácii teda nie je druhá vrstva nad RLS. Pre tohto používateľa je to jediná kontrola.

Keď už ste v tých helperoch, pozrite sa, či kontrolujú `organizations.is_active`. Helpery pre členstvo to často nerobia a potom deaktivovaná organizácia na vrstve RLS naďalej umožňuje čítanie aj zápis. Ak jediná kontrola `is_active` žije v tenant resolveri aplikácie, každý dotaz, ktorý resolver obíde, obíde aj deaktiváciu.

## Čo to opravilo?

Organizácia požiadavky sa stala povinným argumentom každej servisnej funkcie, ktorá číta alebo zapisuje tenantné dáta. Volajúci na ňu nemôže zabudnúť, pretože funkcia bez nej nejde preložiť.

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

Z revízie vyšli ešte dve pravidlá:

- Nikdy neberte organizáciu z riadku, ktorý sa chystáte zapísať. `update ... where id = $1 and organization_id = <organization_id toho istého riadku>` platí vždy, takže nestráži nič.
- Zložený cudzí kľúč na `(id, organization_id)` prinúti databázu odmietnuť väzbu medzi riadkami rôznych organizácií. Skontrolujte každú podriadenú tabuľku, ľahko ho pridáte k trom a štvrtú zabudnete.

## Ako skontrolovať vlastnú aplikáciu?

Vytvorte jedného používateľa s aktívnym členstvom v dvoch organizáciách. Potom v SQL editore lokálnej alebo stagingovej kópie konajte ako on a vyžiadajte si riadok druhej organizácie:

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

Zopakujte to s `update` a ešte raz po nastavení druhej organizácie ako neaktívnej. Ak prvý dotaz vráti riadok, prejdite všetky miesta, ktoré dotazujú tenantné dáta iba podľa id.

## Aké je to vážne?

Posúďte to úprimne. Útočník potrebuje skutočné členstvo v oboch organizáciách, takže ide o integritu a auditné stopy: niekto s prístupom k dvom klientom upraví nesprávneho a log hovorí, že to bolo povolené. Nie je to spôsob, ako by cudzí človek čítal všetkých tenantov.

Všeobecné poučenie je to z článku [ako testujem Supabase RLS](https://madsalt.tech/sk/blog/how-to-test-supabase-rls/): testujte, čo skutočný používateľ môže, so skutočnými tvarmi dát, nielen to, čo hovorí každá politika. Tri súvisiace poznámky: [rola prevzatá z metadát pri registrácii](https://madsalt.tech/sk/notes/supabase-raw-user-meta-data-role/), [NULL, ktoré preskočilo kontrolu oprávnení](https://madsalt.tech/sk/notes/plpgsql-if-not-null-fails-open/) a [ktoré schémy Data API ponúka](https://madsalt.tech/sk/notes/supabase-exposed-schemas/). Test dvoch firiem je prvá kontrola môjho [checklistu bezpečnosti aplikácie z Lovable](https://madsalt.tech/sk/blog/lovable-app-security-checklist/). Na politiky, granty a nastavenia okolo toho bezplatná [Kontrola zabezpečenia Supabase](https://madsalt.tech/sk/tools/supabase-security-check/) spustí 46 pravidiel OWASP Top 10:2025; či politika pustí používateľa v dvoch org, je otázka tvaru dát, na ktorú odpovie iba dotaz vyššie.
