# Multi-tenant: el error que no se ve

> Aislar los datos de cada empresa en una sola base parece resuelto hasta que no lo está. Por qué fijar el search_path no alcanza y dónde mirar de verdad.

Crónica N.º 02 · Ingeniería · 2 de septiembre de 2026 · 2 min de lectura
Firma: Juan Pedro Gachet, cofundador y director de Mithandir · https://mithandir.com/blog/multi-tenant-el-error-que-no-se-ve

Cuando un sistema atiende a varias empresas desde una sola base de datos, hay una pregunta que
se responde el primer día y no se vuelve a mirar nunca: **¿cómo garantizamos que la empresa A
no vea los datos de la empresa B?**

En Postgres la respuesta corta es Row Level Security. Se activa RLS en la tabla, se escribe una
política que compara la empresa de la fila contra la del usuario logueado, y listo. Es correcto,
funciona, y es lo que usamos.

El problema aparece después, cuando el sistema crece y alguien necesita hacer algo que la
política no deja hacer.

## La función que se saltea las reglas

Supongamos que hay que calcular un total que cruza varias tablas, o correr un proceso nocturno.
Escribir eso respetando RLS es incómodo, así que aparece la salida fácil: una función
`SECURITY DEFINER`. Esa función corre con los permisos de quien la creó —normalmente el dueño de
la base— y **las políticas de RLS no la frenan**. Es exactamente para eso que existe.

Hasta acá está bien. El problema es qué se revisa después.

La recomendación que todo el mundo repite sobre `SECURITY DEFINER` es fijarle el `search_path`:

```sql
CREATE FUNCTION calcular_total(p_empresa uuid)
RETURNS numeric
LANGUAGE sql
SECURITY DEFINER
SET search_path = public   -- esto es lo que todos revisan
AS $$ ... $$;
```

Eso previene un ataque real: que alguien cree una tabla con el mismo nombre en un esquema que
esté antes en el `search_path` y logre que la función lea la suya. Es una defensa necesaria.

**Pero no tiene nada que ver con quién puede llamar a la función.**

## Dónde está el control de acceso de verdad

Una función `SECURITY DEFINER` esquiva RLS por diseño. Entonces lo único que decide quién puede
ejecutarla es el `GRANT EXECUTE`. Eso es el control de acceso, no el `search_path`.

Y ahí está la trampa: en Postgres, cuando creás una función, el permiso de ejecución se le
otorga a `PUBLIC` por defecto. No hay que hacer nada mal para que quede abierta — hay que
acordarse de cerrarla.

En un proyecto nuestro encontramos tres funciones `SECURITY DEFINER` que tenían el
`search_path` correctamente fijado, pasaban todas las revisiones que hacíamos, y estaban
ejecutables por el rol anónimo. El chequeo automático daba verde porque miraba lo que todos
miran.

La forma de verlo:

```sql
SELECT p.proname,
  p.prosecdef AS es_security_definer,
  pg_get_function_identity_arguments(p.oid) AS args,
  array_to_string(p.proacl, ', ') AS permisos
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public' AND p.prosecdef
ORDER BY p.proname;
```

Si en `permisos` aparece `=X/` sin un rol adelante, o el valor es `NULL`, esa función la puede
llamar cualquiera. Cerrarla es explícito:

```sql
REVOKE EXECUTE ON FUNCTION calcular_total(uuid) FROM PUBLIC;
GRANT  EXECUTE ON FUNCTION calcular_total(uuid) TO authenticated;
```

## Lo que aprendimos

El invariante que revisábamos estaba incompleto. Verificaba que RLS estuviera activo en todas
las tablas y que las funciones `SECURITY DEFINER` tuvieran `search_path` fijo. Le faltaba la
tercera pregunta, que es la que importa: **quién puede ejecutar cada una de esas funciones.**

Es un caso de manual de por qué un chequeo automático que da verde puede ser peor que no tener
ninguno: genera la confianza sin dar la garantía. Ahora el chequeo mira las tres cosas, y corre
en cada deploy.

Si tenés un sistema multi-tenant en Postgres, la consulta de arriba tarda dos segundos. Vale la
pena correrla hoy.

---

Más crónicas: https://mithandir.com/blog · Hablar con el equipo: https://mithandir.com/contacto
