Prejsť na obsah

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

Supabase

Zobraziť ako MarkdownOtvoriť v ClaudeOtvoriť v ChatGPTOtvoriť v Perplexity

Otázka sa tiež skopíruje. Ak sa chat otvorí prázdny, vložte ju.

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, 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“.

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, 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.
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ť.

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:

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: 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, NULL, ktoré preskočilo kontrolu oprávnení a ktoré schémy Data API ponúka. Test dvoch firiem je prvá kontrola môjho checklistu bezpečnosti aplikácie z Lovable. Na politiky, granty a nastavenia okolo toho bezplatná Kontrola zabezpečenia Supabase 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.

© 2026 Denys Zaichenko. Pôvodný článok: https://madsalt.tech/sk/notes/supabase-rls-user-in-two-organizations/