---
title: "Supabase RLS multi-tenant: utente in due org, dati esposti"
description: "Una policy RLS che verifica l'appartenenza a un'org è vera in entrambe per chi appartiene a due org. Cosa ha trovato la sonda, la correzione e un controllo."
url: "https://madsalt.tech/it/notes/supabase-rls-user-in-two-organizations/"
author: "Denys Zaichenko"
language: "it"
published: 2026-10-18
---

# Supabase RLS multi-tenant: utente in due org, dati esposti

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

```ts
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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html), 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.

![L'org della richiesta non è nella policy. Un utente, membro dell'org A e dell'org B, che lavora nell'org A. Solo la policy RLS: Richiesta in org A → Membro della sua org? → Sì, A o B → Riga org B restituita; RLS più l'org di questa richiesta: Richiesta in org A → Membro della sua org? → Riga in org A? → 0 righe org B.](https://madsalt.tech/charts/it/supabase-rls-user-in-two-organizations.svg)

*La mia sonda su un utente di prova in due org, settembre 2026: con la sola RLS lettura e scrittura sul documento dell'org B; con l'org della richiesta, 0 righe.*

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.

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

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

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](https://madsalt.tech/it/blog/how-to-test-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](https://madsalt.tech/it/notes/supabase-raw-user-meta-data-role/), [un NULL che ha saltato un controllo dei permessi](https://madsalt.tech/it/notes/plpgsql-if-not-null-fails-open/) e [quali schemi serve la Data API](https://madsalt.tech/it/notes/supabase-exposed-schemas/). Il test delle due aziende è il primo punto della mia [checklist di sicurezza per le app Lovable](https://madsalt.tech/it/blog/lovable-app-security-checklist/). Per policy, grant e impostazioni intorno, il [Controllo sicurezza Supabase](https://madsalt.tech/it/tools/supabase-security-check/) 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.
