In breve
Una policy come auth_in_org(organization_id) chiede “il chiamante è membro dell’org proprietaria di questa riga?”. Per un utente che appartiene all’org A e all’org B, la risposta è sì in entrambe. Nella mia sonda, con la sola RLS un utente così aveva accesso in lettura e scrittura a un documento dell’altra org. Aggiungendo alla query l’org della richiesta corrente, il risultato è stato 0 righe.
L’ho scoperto mentre rafforzavo la piattaforma documentale per l’edilizia, un’app Supabase multi-tenant. Ogni tabella dei tenant aveva la row level security attiva e forzata, quindi sembrava finita.
Cosa sembrava sicuro?
Ogni policy aveva la stessa forma: l’organization_id della riga deve essere un’org di cui il chiamante è membro. Per questo alcune server action cercavano un documento solo per id. Il ragionamento era “la RLS filtra comunque”.
const { data } = await supabase
.from('documents')
.select('*')
.eq('document_id', id)
.single()
Per un utente con una sola appartenenza funziona, e un test con quell’utente passa.
Perché la RLS lascia passare l’altra org?
La policy è un test di appartenenza. Postgres la valuta per ogni riga, come descrive la documentazione sulla row security, e non sa per conto di quale organizzazione agisce la richiesta, perché questo vive nell’app: nell’URL, in un cookie o nel selettore di org. Per un utente membro di entrambe le org, tutte le righe di entrambe passano.
Quindi il filtro per organization_id nell’app non è un secondo livello sopra la RLS. Per questo utente è l’unico controllo.
Mentre è su quegli helper, controlli se guardano organizations.is_active. Gli helper di appartenenza spesso non lo fanno, e allora un’org disattivata mantiene accesso in lettura e scrittura a livello RLS. Quando l’unico controllo di is_active vive nel resolver di tenant dell’app, qualsiasi query che salta il resolver salta anche la disattivazione.
Cosa l’ha risolto?
L’org della richiesta è diventata un argomento obbligatorio di ogni funzione di servizio che legge o scrive dati dei tenant. Un punto di chiamata non può dimenticarla, perché senza non compila.
export async function getDocument(orgId: string, id: string) {
return supabase
.from('documents')
.select('*')
.eq('organization_id', orgId)
.eq('document_id', id)
.single()
}
Dalla revisione sono uscite altre due regole:
- Non prenda mai l’org dalla riga che sta per scrivere.
update ... where id = $1 and organization_id = <l'organization_id della riga stessa>è sempre vero, quindi non protegge nulla. - Una chiave esterna composta su
(id, organization_id)fa rifiutare al database un collegamento tra righe di org diverse. Controlli ogni tabella figlia: è facile aggiungerla a tre e dimenticare la quarta.
Come controllare la sua app?
Crei un utente con un’appartenenza attiva in due org. Poi, nell’editor SQL di una copia locale o di staging, agisca come quell’utente e chieda una riga dell’altra org:
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;
Lo ripeta con un update, e ancora dopo aver reso inattiva la seconda org. Se la prima query restituisce una riga, guardi ogni punto che interroga i dati dei tenant solo per id.
Quanto è grave?
Lo dimensioni onestamente. L’attaccante ha bisogno di un’appartenenza reale in entrambe le org, quindi si tratta di integrità e tracce di audit: chi ha accesso a due clienti modifica quello sbagliato, e il log dice che era consentito. Non è un modo per uno sconosciuto di leggere tutti i tenant.
La lezione generale è quella di come testo la Supabase RLS: si testi ciò che un utente reale può fare, con dati reali, non solo ciò che dice ogni policy. Tre note collegate: un ruolo preso dai metadati di registrazione, un NULL che ha saltato un controllo dei permessi e quali schemi serve la Data API. Il test delle due aziende è il primo punto della mia checklist di sicurezza per le app Lovable. Per policy, grant e impostazioni intorno, il Controllo sicurezza Supabase gratuito esegue 46 regole OWASP Top 10:2025; se una policy lascia passare un utente di due org è una questione di forma dei dati a cui risponde solo la query qui sopra.