TL;DR
Politiikka kuten auth_in_org(organization_id) kysyy “onko kutsuja sen organisaation jäsen, joka omistaa tämän rivin?”. Käyttäjälle, joka kuuluu organisaatioon A ja organisaatioon B, vastaus on kyllä molemmissa. Koettimessani pelkkä RLS antoi tällaiselle käyttäjälle luku- ja kirjoitusoikeuden toisen organisaation dokumenttiin. Kun lisäsin kyselyyn nykyisen pyynnön organisaation, tulokseksi tuli 0 riviä.
Löysin tämän koventaessani rakennusdokumenttialustaa, monivuokraajaista Supabase-sovellusta. Jokaisessa vuokralaistaulussa row level security oli päällä ja pakotettu, joten kaikki näytti valmiilta.
Mikä näytti turvalliselta?
Jokaisella politiikalla oli sama muoto: rivin organization_id on oltava organisaatio, jonka jäsen kutsuja on. Siksi muutama server action haki dokumentin pelkällä id:llä. Päättely oli “RLS suodattaa sen joka tapauksessa”.
const { data } = await supabase
.from('documents')
.select('*')
.eq('document_id', id)
.single()
Käyttäjällä, jolla on yksi jäsenyys, tämä toimii, ja testi tällaisella käyttäjällä menee läpi.
Miksi RLS päästää toisen organisaation läpi?
Politiikka on jäsenyystesti. Postgres arvioi sen jokaiselle riville, kuten row security -dokumentaatio kuvaa, eikä se tiedä, minkä organisaation nimissä pyyntö toimii, koska se asuu sovelluksessa: URL:ssä, evästeessä tai organisaatiovalitsimessa. Käyttäjälle, joka on molempien organisaatioiden jäsen, kummankin organisaation jokainen rivi pääsee läpi.
Sovelluksen organization_id-suodatin ei siis ole toinen kerros RLS:n päällä. Tälle käyttäjälle se on ainoa kontrolli.
Kun olet noissa apufunktioissa, tarkista, katsovatko ne organizations.is_active. Jäsenyysapurit eivät usein katso, ja silloin deaktivoitu organisaatio säilyttää luku- ja kirjoitusoikeuden RLS-tasolla. Kun ainoa is_active-tarkistus on sovelluksen tenant-resolverissa, jokainen kysely, joka ohittaa resolverin, ohittaa myös deaktivoinnin.
Mikä korjasi sen?
Pyynnön organisaatiosta tuli pakollinen argumentti jokaiseen palvelufunktioon, joka lukee tai kirjoittaa vuokralaisdataa. Kutsukohta ei voi unohtaa sitä, koska funktio ei käänny ilman sitä.
export async function getDocument(orgId: string, id: string) {
return supabase
.from('documents')
.select('*')
.eq('organization_id', orgId)
.eq('document_id', id)
.single()
}
Katselmoinnista tuli kaksi sääntöä lisää:
- Älä koskaan ota organisaatiota riviltä, jota olet kirjoittamassa.
update ... where id = $1 and organization_id = <that row's own organization_id>on aina tosi, joten se ei suojaa mitään. - Yhdistetty vierasavain
(id, organization_id)saa tietokannan kieltäytymään linkistä eri organisaatioiden rivien välillä. Tarkista jokainen lapsitaulu: sen lisää helposti kolmeen ja unohtaa neljännen.
Miten tarkistat oman sovelluksesi?
Luo yksi käyttäjä, jolla on aktiivinen jäsenyys kahdessa organisaatiossa. Toimi sitten tuona käyttäjänä paikallisen tai staging-kopion SQL-editorissa ja pyydä toisen organisaation rivi:
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;
Toista update-lauseella ja vielä kerran sen jälkeen, kun olet asettanut toisen organisaation epäaktiiviseksi. Jos ensimmäinen kysely palauttaa rivin, katso jokainen paikka, joka hakee vuokralaisdataa pelkällä id:llä.
Kuinka paha tämä on?
Arvioi mittakaava rehellisesti. Hyökkääjä tarvitsee aidon jäsenyyden molemmissa organisaatioissa, joten kyse on eheydestä ja auditointilokeista: joku, jolla on pääsy kahteen asiakkaaseen, muokkaa väärää, ja loki sanoo sen olleen sallittua. Tämä ei ole tapa, jolla ulkopuolinen lukisi kaikki vuokralaiset.
Yleinen opetus on sama kuin tekstissä miten testaan Supabase RLS:ää: testaa, mitä oikea käyttäjä voi tehdä, oikeilla datan muodoilla, älä vain sitä, mitä kukin politiikka sanoo. Kolme aiheeseen liittyvää muistiinpanoa: rekisteröitymismetadatasta otettu rooli, NULL, joka ohitti oikeustarkistuksen ja mitkä skeemat Data API tarjoaa. Kahden yrityksen testi on Lovable-sovelluksen tietoturvatarkistuslistani ensimmäinen kohta. Politiikkojen, oikeuksien ja asetusten osalta ilmainen Supabase tietoturvatarkistus ajaa 46 OWASP Top 10:2025 -sääntöä; päästääkö politiikka kahden organisaation käyttäjän läpi, on datan muotoon liittyvä kysymys, johon vastaa vain yllä oleva kysely.