---
title: "Supabase RLS multi-tenant: usuario en dos orgs filtra datos"
description: "Una política RLS que comprueba la pertenencia es verdadera en las dos organizaciones para un usuario que está en ambas. Mi sonda, la corrección, una prueba."
url: "https://madsalt.tech/es/notes/supabase-rls-user-in-two-organizations/"
author: "Denys Zaichenko"
language: "es"
published: 2026-10-18
---

# Supabase RLS multi-tenant: usuario en dos orgs filtra datos

En resumen

Una política como `auth_in_org(organization_id)` pregunta: "¿es quien llama miembro de la organización dueña de esta fila?". Para un usuario que pertenece a la org A y a la org B, la respuesta es sí en las dos. En mi sonda, RLS por sí sola daba a ese usuario acceso de lectura y escritura a un documento de la otra organización. Al añadir a la consulta la organización de la petición en curso, devolvía 0 filas.

Lo encontré mientras reforzaba la [plataforma de documentos de obra](https://madsalt.tech/es/work/construction-document-platform/), una aplicación Supabase multi-tenant. Cada tabla de inquilino tenía el row level security activado y forzado, así que parecía terminada.

## ¿Qué parecía seguro?

Todas las políticas tenían la misma forma: el `organization_id` de la fila debe ser una organización de la que quien llama es miembro. Por eso, algunas server actions buscaban un documento solo por su id. El razonamiento era "RLS lo filtrará de todos modos".

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

Para un usuario con una sola pertenencia esto funciona, y una prueba con un usuario así pasa.

## ¿Por qué RLS deja pasar a la otra organización?

La política es una prueba de pertenencia. Postgres la evalúa para cada fila, como describe la [documentación de row security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html), y no sabe en nombre de qué organización actúa la petición, porque eso vive en la aplicación: en la URL, en una cookie o en el selector de organización. Para un usuario que es miembro de las dos organizaciones, pasan todas las filas de ambas.

![La org de una petición no está en la política. Un usuario, miembro de la org A y de la org B, trabaja en la org A. Solo la política RLS: Petición en org A → ¿Miembro de su org? → Sí, A o B → Fila de org B; RLS más la org de esta petición: Petición en org A → ¿Miembro de su org? → ¿Fila de org A? → 0 filas de org B.](https://madsalt.tech/charts/es/supabase-rls-user-in-two-organizations.svg)

*Mi sonda con un usuario de prueba en dos orgs, septiembre de 2026: RLS sola daba lectura y escritura sobre el documento de la org B; con la org de la petición devolvía 0 filas.*

Así que el filtro `organization_id` de la aplicación no es una segunda capa sobre RLS. Para este usuario es el único control.

Ya que está en esos helpers, compruebe si miran `organizations.is_active`. Los helpers de pertenencia a menudo no lo hacen, y entonces una organización desactivada conserva acceso de lectura y escritura en la capa de RLS. Cuando la única comprobación de `is_active` vive en el resolvedor de inquilino de la aplicación, cualquier consulta que se salte el resolvedor se salta también la desactivación.

## ¿Qué lo arregló?

La organización de la petición pasó a ser un argumento obligatorio de cada función de servicio que lee o escribe datos de inquilino. Un punto de llamada no puede olvidarla, porque la función no compila sin ella.

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

De la revisión salieron dos reglas más:

- Nunca tome la organización de la fila que está a punto de escribir. `update ... where id = $1 and organization_id = <that row's own organization_id>` es siempre verdadero, así que no protege nada.
- Una clave foránea compuesta sobre `(id, organization_id)` hace que la base de datos rechace un vínculo entre filas de organizaciones distintas. Revise cada tabla hija: es fácil ponerla en tres y olvidar la cuarta.

## ¿Cómo comprobar su propia aplicación?

Cree un usuario con una pertenencia activa en dos organizaciones. Luego, en el editor SQL de una copia local o de staging, actúe como ese usuario y pida una fila de la otra organización:

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

Repítalo con un `update`, y otra vez después de marcar la segunda organización como inactiva. Si la primera consulta devuelve una fila, revise cada lugar que consulta datos de inquilino solo por id.

## ¿Qué gravedad tiene?

Dimensiónelo con honestidad. El atacante necesita una pertenencia real a las dos organizaciones, así que se trata de integridad y de rastros de auditoría: alguien con acceso a dos clientes edita el equivocado, y el registro dice que estaba permitido. No es una forma de que un extraño lea a todos los inquilinos.

La lección general es la de [cómo pruebo el RLS de Supabase](https://madsalt.tech/es/blog/how-to-test-supabase-rls/): pruebe lo que un usuario real puede hacer, con datos reales, no solo lo que dice cada política. Tres notas relacionadas: [un rol tomado de los metadatos del registro](https://madsalt.tech/es/notes/supabase-raw-user-meta-data-role/), [un NULL que se saltó una comprobación de permisos](https://madsalt.tech/es/notes/plpgsql-if-not-null-fails-open/) y [qué esquemas sirve la Data API](https://madsalt.tech/es/notes/supabase-exposed-schemas/). La prueba de las dos empresas es el primer control de mi [lista de seguridad para apps Lovable](https://madsalt.tech/es/blog/lovable-app-security-checklist/). Para las políticas, grants y ajustes que la rodean, la [Auditoría de seguridad de Supabase](https://madsalt.tech/es/tools/supabase-security-check/) gratuita ejecuta 46 reglas de OWASP Top 10:2025; si una política deja pasar a un usuario de dos orgs es una cuestión de forma de los datos que solo responde la consulta de arriba.
