Volver a la wiki

Raíz R6: Fallback silencioso a primer tenant permitía lectura/escritura cross-tenant (CERRADO)

Resumen ejecutivo

Severidad: CRÍTICA (aislamiento multi-tenant quebrado)
Status: 🟢 CERRADO (2026-06-11, commit e786894)
Período afectado: Desde refactor a Django (mono-tenant → multi-tenant incompleto)
Usuarios en riesgo: Cualquier principal autenticado sin organización asignada, o endpoints legacy anónimos

Qué pasaba

El código heredado contenía Organization.objects.first() como fallback en ~26 call-sites. Consecuencia: un principal sin org caía automáticamente al primer tenant de la BD.

Escenarios explorados

Lectura cross-tenant (confirmado)

Escritura cross-tenant (confirmado)

Raíz causa

  1. Arquitectura heredada: código mono-empresa que usaba Organization.objects.first() como “la” org por defecto
  2. Refactor incompleto: al pasar a multi-tenant, no se eliminaron los fallbacks; solo se agregó request.user.organization
  3. Falta de validación: endpoints read-only retornaban datos, endpoints write permitían cambios, sin validar org context

Cómo se encontró

Auditoría Suprema (2026-06): reporte de seguridad R6 flagged “Aislamiento multi-tenant incompleto”. Análisis manual de core/api/*, core/views, racks/views → encontrados 26 call-sites.

Solución

Tanda T5 (2026-06-11): eliminación del fallback + fail-closed design:

  1. ✅ get_current_org(request) retorna None si no hay org (no intenta adivinar)
  2. ✅ require_org(request) nuevo helper: lanza 403 si no hay org
  3. ✅ Endpoints read-only: retornan listas vacías, defaults, o counts = 0
  4. ✅ Endpoints write: lanzan 404 si get_current_org() es None
  5. ✅ Vistas (views.py): dashboard y reports muestran vacío
  6. ✅ Bonus: rack_editor ahora scoped a org (antes get_object_or_404(Rack, id=...) sin org)

Cambios de código

Eliminación de fallback (~26 sites):

# ✗ ANTES (inseguro)
org = request.user.organization or Organization.objects.first()

# ✓ DESPUÉS (fail-closed)
org = get_current_org(request)  # retorna None si no hay org
if not org:
    return []  # read-only: vacío
    # o raise HttpError(404, ...) para write

Promoción de utility:

Scope de rack_editor:

# ✗ ANTES
rack = get_object_or_404(Rack, id=rack_id)  # sin org

# ✓ DESPUÉS
org = request.user.organization if hasattr(...) else None
rack = get_object_or_404(Rack, id=rack_id, organization=org)  # 404 cross-tenant

Testing

14 tests nuevos (tests/api/test_t5_org_fallback.py):

Impacto

Breaking change: Cualquier cliente/bot que dependiera de Organization.objects.first() fallará (intencional).

No-impact: Todo principal normalmente tiene org asignada. Solo afecta a:

Status actual (2026-06-11)

Véase también

Subir