CreaRack-SL

Auditoría Suprema 2 · Cola racks: librería de stencils, backup/restore, plantillas y rastro de auditoría

Cuándo

04-09-2026 · commit 17879889 (PR #500, v1.109.0). Cola MEDIA/BAJA del dominio racks en la Auditoría Suprema 2 (task #286): de los 29 hallazgos MEDIA del catálogo auditoria-suprema-2/racks.json, 14 se cierran en este commit, 8 ya estaban resueltos por la Tanda 7 genérica, y 6 quedan diferidos con motivo explícito en el CHANGELOG. Los 7 hallazgos ALTA del mismo catálogo NO se tocan en este PR — ver Preventivos futuros. Implementación en Opus (worktree propio, en paralelo con los PRs de network y signage); revisión de Fable 5.1 antes de mergear. mypy limpio en racks + network/api/port_connections.py (48 ficheros).

Síntomas visibles

  1. PUT /api/racks/{id}/devices devolvía al navegador el management_config cifrado del equipo (#8).
  2. apply_template no validaba la altura del rack destino ni invalidaba la caché de roll-ups: una plantilla de 42U sobre un rack de 12U dejaba equipos en U40 (#10).
  3. Cada autosave del editor descifraba con Fernet TODOS los management_config de la organización para construir la caché de IPs, aunque el payload no trajera ninguna IP (#12).
  4. El envío a la papelera desde el editor — el camino más destructivo del dominio — no dejaba autor ni rastro de auditoría (#17).
  5. El restore extraía uploads/** del ZIP tal cual sobre un namespace plano común a todas las organizaciones: podía sobrescribir un fichero vivo de otro tenant (#22).
  6. Ni el restore completo ni el de librería tenían tope de subida, ni la descompresión del ZIP tenía techo (zip-bomb) (#23).
  7. import_image construía la ruta de disco con la category que manda el cliente: una cadena "../../.." escapaba de MEDIA_ROOT (#24).
  8. import_image guardaba un image_path de /static/ mientras escribía el fichero en MEDIA_ROOT: el stencil nacía con la miniatura rota y la API respondía 200 igualmente (#25).
  9. El backup exportaba parent_rack_id pero el restore nunca lo aplicaba: la jerarquía de racks se perdía y la verificación decía complete (#26).
  10. Nadie podaba MEDIA_ROOT/temp/: cada sesión de Visio o de librería abandonada dejaba el ZIP descomprimido en el volumen para siempre (#27).
  11. El backup completo, el restore y las mutaciones de librería no dejaban ninguna fila en el audit log (#28).
  12. create_from_device, import_visio y cleanup_session mutaban la librería del tenant sin exigir racks:admin, a diferencia de sus hermanos (#29).
  13. create_custom_stencil cargaba el fichero entero en memoria (ContentFile(read())) sin validar tipo ni tamaño (#31).
  14. La foto diaria de integridad se calculaba para TODAS las organizaciones, inactivas incluidas, y ninguna tarea purgaba las filas acumuladas (#34).
  15. Los campos de cableado no tenían max_length en el schema Ninja pero sí en la columna: un texto largo devolvía 500 en vez de 400 (#36).
  16. (Sin numerar, hallado al escribir los tests) las rutas estáticas /stencils/custom, /create_from_image y /create_from_device estaban registradas DESPUÉS de /stencils/{stencil_id}: Django Ninja resuelve por orden y POST /stencils/custom respondía 405 — endpoint muerto.

Causa raíz

  • Rutas de fichero construidas con input del cliente sin confinar contra MEDIA_ROOT en todos los caminos de escritura (import_image, uploads/** del restore).
  • Mutadores de librería añadidos sin heredar el gate racks:admin de sus hermanos — cada endpoint nuevo repetía la comprobación a mano.
  • Validaciones de negocio duplicadas en vez de reutilizar el helper compartido: apply_template reimplementaba la copia de equipos sin las comprobaciones que copy_devices_from ya tenía.
  • Ningún camino que acepta un ZIP subido por el usuario tenía tope de tamaño ni de descompresión.
  • Acciones destructivas o de bajo nivel (papelera, backup, restore, borrado de categoría) sin paso por log_rack_action: el audit log solo cubría los mutadores más antiguos del dominio.
  • Orden de registro de rutas en el router: Django Ninja resuelve por posición y una ruta dinámica registrada antes que las estáticas las sombrea en silencio.
  • Tareas periódicas sin filtro de organizaciones activas ni política de retención.

Fix aplicado

Commit 17879889 (PR #500):

  • racks/schemas.DeviceSaveResultSchema deja de exponer management_config; la caché de IPs del autosave (racks/services/racks.py) solo se construye si el payload trae IP.
  • apply_template delega en copy_devices_from(target, template, "replace"): valida altura, invalida roll-ups y la respuesta trae copied/skipped.
  • racks/utils/safe_fs.py: MAX_UNCOMPRESSED_BYTES en safe_extract_zip; restore.py añade MAX_RESTORE_UPLOAD_BYTES (2 GB) y MAX_BACKUP_DATA_BYTES (512 MB); library_files.py topa el restore de librería a 200 MB; create_custom_stencil a 25 MB.
  • restore.py: un miembro de uploads/** cuyo destino ya existe no se sobrescribe.
  • network_config.py (import_image): categoría normalizada + is_within_directory, SVG saneado, image_path apunta a /media.
  • restore_inventory.py: segunda pasada dentro de la misma transacción que aplica parent_rack_id con el rack_map completo.
  • racks/tasks.py: nueva task periódica cleanup_media_temp (03:50, umbral 24 h); snapshots de integridad filtran is_active y purgan a 90 días.
  • log_rack_action añadido en trash_device/bulk_trash, backup.start, backup.download, restore.full, library.backup, library.restore, library.category.delete.
  • require_perm(request, "racks", "admin") añadido en create_from_device, import_visio, cleanup_session.
  • racks/api/library.py: rutas estáticas de stencils reordenadas antes que /stencils/{stencil_id}.
  • network/api/port_connections.py: max_length en los campos de cableado del schema Ninja.
  • 20 tests nuevos en tests/racks/test_cola_auditoria_racks.py; regresión del área en verde (281 + 36 tests).

Lecciones

  • Un endpoint nuevo que muta la misma entidad que sus hermanos hereda su gate de permiso por convención, no por defecto — sin un decorador o mixin compartido, cada alta puede olvidarlo.
  • Cualquier camino que acepta un ZIP de usuario necesita tope de subida Y de descompresión desde el primer commit: el zip-bomb no es un caso límite, es el caso por defecto a defender.
  • Un router que resuelve por orden de registro (Django Ninja) exige que las rutas estáticas vayan primero; un test de integración por ruta lo habría atrapado antes de producción.
  • La papelera y el rastro de auditoría no son gratis: cada acción destructiva nueva necesita su propia línea de log_rack_action, no hereda la del endpoint que la inspiró.

Preventivos futuros

  • 7 hallazgos ALTA de racks.json siguen sin tocar, ninguno cubierto por este PR: path traversal en el restore de librería (library_files.confirm_restore_backup, el campo file del manifest copia cualquier fichero del servidor); backups nocturnos de TODOS los tenants descargables por cualquier usuario autenticado (/media/backups/<org_id>/...); confirm_visio_import puede robar o destruir ficheros de OTRAS organizaciones; _ensure_media_file del restore lee/escribe sin confinar origen ni destino; cuatro caminos que persisten SVG en la librería sin sanitizar; el radar de integridad sirve el inventario de red (IP, MAC, nº de serie) sin exigir network:view; y clone_rack duplica el management_config — el clon comparte IP de gestión con el original y, con la papelera nueva, retirar el clon borra el original.
  • 6 hallazgos MEDIA diferidos con motivo (detalle en CHANGELOG v1.109.0): #18 borrados que se saltan la papelera (copy_from replace, vaciar papelera de racks), #20 IP de gestión en columna propia, #21 contrato delta del editor, #30 restore_preview síncrono, #33 /overview sin caché, #35 port_key sin validar contra los puertos del equipo.
  • El re-mapeo de uploads/ por tenant (#22) queda a medias: se evita la sobrescritura, pero no se migran las rutas ya guardadas en Stencil.image_path/Blueprint.image_path — decisión pendiente de Edu.

Véase también

  • [[incident—20260831—auditoria-suprema-2-tanda1-monitoring-racks]]
  • [[incident—20260602—auditoria-suprema-racks]]
  • [[incident—20260904—auditoria-suprema-2-cola-monitoring-a-sondas-y-targets]]
  • [[entity—racks—service—apply-full-restore-from-zip]]
  • [[entity—racks—service—safe-fs]]
  • [[entity—racks—service—run-full-backup]]
  • [[entity—racks—model—stencil]]
  • [[feature—racks—full-backup-tenant-scoping]]
  • [[feature—racks—validacion-entrada-library-r7p1a]]
  • [[crearack—racks—trash-and-restore]]