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_gatedencore/views.pyreemplaza el patrón genéricodjango.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_ROOTresuelto 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:RateLimitMiddlewaretrasAuthenticationMiddleware. - 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_credentialscon 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 retiraDEV_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ónserve_media_gatedcon excepciónsignage/<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
- Desplegar a staging con tests verdes.
- Verificar en PROD que anónimos no pueden descargar media.
- Ejecutar
./manage.py reencrypt_credentials --dry-runen PROD. - Ejecutar sin
--dry-runtras validar reporte. - PR posterior: remover
DEV_KEYy 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]]