CreaRack-SL

Auditoría Suprema Etapa 3: Cierre de findings M1 (media gateado), B6 (orden middleware) y E (Device.management_config)

Contexto

Bloque de cierre de 3 findings menores (pero críticos) del informe Auditoría Suprema, que pertenecen a la tanda transversal config/IA (configuración de seguridad y flujos de autenticación). Precedente: decisión de retirar DEV_KEY tras completar cobertura de re-cifrado en todos los almacenes de credenciales.

Problem statement

M1: /media/ sin control de acceso (anonimato = obscuridad)

Hallazgo: El endpoint /media/ servía cualquier fichero subido (planos, stencils, assets) a cualquiera que tuviera el UUID del nombre. No había validación de sesión ni de tenant — la “seguridad” era solo el nombre impredecible del archivo.

Riesgo: Un usuario de Tenant A podía descargar planos/imágenes de Tenant B si adivinaba o interceptaba la URL.

Solución adoptada:

  • Nueva función serve_media_gated en core/views.py reemplaza el patrón genérico django.views.static.serve.
  • Requisito: request.user.is_authenticated.
  • Anónimos reciben 404 (no 403) para no revelar existencia del fichero.
  • Única excepción pública: signage/<org>/thumbs/ (320px, portal de cliente con share-token).
  • MEDIA_ROOT resuelto en runtime (respeta overrides de tests).

Verificación: Los players SpinetiX usan views_publish con FileResponse propio, no acceden a /media directamente.

B6: RateLimitMiddleware en orden incorrecto

Hallazgo: El middleware de rate-limit por tenant corría antes de AuthenticationMiddleware, por lo que request.user no estaba aún disponible. La capa per-tenant (que resuelve request.user.organization) era código muerto — solo el límite per-IP funcionaba.

Riesgo: Sin límite por tenant, un usuario de Tenant A podía consumir la cuota de otro tenant.

Solución adoptada:

  • Reordenar en config/settings/base.py: RateLimitMiddleware tras AuthenticationMiddleware.
  • Ambas capas (per-IP + per-tenant) operan ahora.

E: Device.management_config no cubierto en re-cifrado

Hallazgo: El campo racks.Device.management_config almacena credenciales (ip, username, password, enable_password) en JSON cifrado, pero no estaba incluido en el comando reencrypt_credentials.

Riesgo: Al retirar DEV_KEY del fallback de descifrado, estos dispositivos quedarían inaccesibles.

Solución adoptada:

  • Extender reencrypt_credentials con método _process_device_management_configs.
  • Itera sobre Device con config no vacío, re-cifra el JSON top-level + el sub-objeto “connection”.
  • 1 test: Device con password cifrado en legacy → re-encriptado.

Decisión

✅ Implementar M1, B6 y E completos en una sesión (Sesión 128, 2026-06-11).

  • M5 (CSRF API) diferido a tanda frontend: exigir header custom rompería fetch directos sin ApiService; SameSite=Strict sigue siendo barrera activa.
  • Tras desplegar a PROD: ejecutar reencrypt_credentials, validar, PR aparte que retira DEV_KEY + fallback.

Implantación

Archivos modificados

  • config/settings/base.py: orden middleware.
  • config/urls.py: nuevo pattern /media/ → serve_media_gated.
  • core/views.py: función serve_media_gated con excepción signage/<org>/thumbs/.
  • core/management/commands/reencrypt_credentials.py: método _process_device_management_configs + docstring.
  • tests/api/test_config_ia_diferidos.py: 5 tests (anónimo 404, auth 200, thumbnail público, scope exception, orden middleware).

Rollout

  1. Desplegar a staging con tests verdes.
  2. Verificar en PROD que anónimos no pueden descargar media.
  3. Ejecutar ./manage.py reencrypt_credentials --dry-run en PROD.
  4. Ejecutar sin --dry-run tras validar reporte.
  5. PR posterior: remover DEV_KEY y fallback del cipher.

Rationale

  • Seguridad en profundidad: no confiar en la obscuridad del nombre (UUID).
  • Autenticación temprana: rate-limit por tenant exige saber quién es el usuario.
  • Cobertura completa: antes de retirar legacy key, todos los almacenes cifrados deben estar migrados.
  • Reversibilidad: tests garantizan que incluso con legacy key, re-cifrado funciona.

Véase también

  • [[entity—core—function—serve-media-gated]]
  • [[entity—core—command—reencrypt-credentials]]
  • [[concept—saas—multi-tenancy]]
  • [[concept—security—authentication-first]]