Saltar a contenido

Auditoría de seguridad multi-tenant — 2026-08-31

Alcance: API Commerce, PostgreSQL/RLS, frontend y contrato interno con TimeLiber Connect.
Estado: hallazgos P0/P1 corregidos, cubiertos en PostgreSQL efímero y desplegados en la base persistente como 0005_tenant_hardening.

Resultado

La aplicación ya verificaba sesión, membresía y capability antes de acceder a integraciones. La auditoría encontró brechas en las barreras secundarias: una política de inserción de membresías demasiado amplia, relaciones que no garantizaban el mismo organization_id, caché de navegador persistente entre cuentas y una prueba RLS cuyo teardown dependía de un downgrade irreversible.

Correcciones

Identidad y RLS

  • Crear una organización establece primero un contexto UUID nuevo y usa ese mismo UUID para organización y membresía propietaria.
  • La política de membresía sólo permite insertar al usuario actual como owner dentro de ese contexto de creación.
  • Una membresía con deleted_at ya no concede visibilidad a la organización.
  • Sin contexto, las políticas continúan fallando cerradas.

Integridad relacional

  • integration_connections expone la clave candidata (id, organization_id).
  • Instalaciones Shopify y receipts usan foreign keys compuestas (connection_id, organization_id).
  • Intents y conexiones exigen una membresía (organization_id, user_id) existente para el usuario asociado.

Estas constraints impiden grafting entre tenants incluso si un repository futuro olvida comprobar ambos lados de la relación.

Frontera del navegador

Al establecer o terminar una sesión se eliminan todas las queries no-auth de TanStack Query. De esta forma una segunda cuenta en el mismo navegador no puede recibir temporalmente organizaciones, conexiones o pedidos de la cuenta anterior desde memoria local.

Frontera Commerce ↔ conector

El JWT para lectura de pedidos incluye organization_id, shop y el subject exacto shopify-orders-read. TimeLiber Connect compara el dominio firmado con el body antes de abrir la sesión Shopify. El endpoint de linking exige el subject shopify-installation-link.

Evidencia automatizada

tests/integration/test_tenant_rls.py levanta PostgreSQL 17 desechable, migra a head y comprueba:

  • lectura sólo del tenant activo;
  • membresía eliminada sin visibilidad;
  • rechazo de insert dirigido a otro tenant;
  • rechazo de autoafiliación como owner a otra empresa;
  • rechazo de webhook que referencia una conexión de otro tenant;
  • creación válida de organización + owner mediante el repository real;
  • imposibilidad de desactivar RLS;
  • roles sin SUPERUSER ni BYPASSRLS.

El contenedor efímero se destruye al terminar; no se ejecuta downgrade de 0004, porque esa migración es intencionalmente irreversible.

Riesgo residual

  • Los JWT internos usan secreto HMAC compartido. La evolución recomendada sigue siendo workload identity o mTLS y replay cache para operaciones mutantes.
  • users es identidad global, no tabla tenant. El runtime conserva grants para registro/autenticación; no debe añadirse una ruta de listado global.
  • Cualquier futura gestión de equipo necesita políticas y pruebas específicas; la política actual sólo cubre la creación del primer owner.

Gate

Ningún nuevo dominio comercial se acepta si carece de:

  1. filtro explícito organization_id en repository;
  2. RLS ENABLE/FORCE;
  3. claves relacionales que preserven el tenant en ambos lados;
  4. prueba de lectura, escritura y relación cross-tenant sobre PostgreSQL real;
  5. limpieza de caché cuando cambia la identidad autenticada.