Volver a la wiki

Auditoría Suprema 2 · Tanda 3: XSS almacenado en el portal de signage, el composer SVG y el editor

Cuándo

31-08-2026 · commit 5d158046 (PR #480, v1.94.0). Tercera tanda de la Auditoría Suprema 2 — la primera (v1.90.0/v1.91.0) cerró huecos de control de acceso en core, monitoring y racks ([[feature—security—auditoria-suprema-2-tanda-1-core]], [[incident—20260831—auditoria-suprema-2-tanda1-monitoring-racks]]). Esta entrega cambia de familia: XSS almacenado en la ruta pública (sin sesión) de Digital Signage y en el editor de racks.

Síntomas visibles

6 hallazgos ALTA + 2 MEDIA, todos en signage salvo el del editor de core:

  1. Portal público de cliente (templates/signage/client_portal.html + signage/views.py): devices_json/playlists_json se inyectaban con |safe dentro de un <script> inline, en una ruta anónima y además exenta de CSP — un nombre de playlist o un hostname con </script><script>… ejecutaba JS arbitrario en el origen de la app.
  2. Composer de SVG (signage/services/svg_composer.py): un fix previo (A4) ya saneaba color/fuente/anchor/weight, pero x/y/width/height, duration, opacity y speed se interpolaban crudos en atributos del SVG servido como image/svg+xml — y position es una clave whitelisteada que el portal anónimo puede fijar.
  3. Saneo de SVG subido por extensión, no por MIME (signage/services/file_storage.py): la neutralización de <script>/on*=/javascript: solo corría si el content_type declarado por el cliente era image/svg+xml — pero el fichero se sirve por su extensión, así que bytes SVG declarados image/png con nombre x.svg se servían con el script vivo.
  4. XSS almacenado en el editor de racks (templates/editor.html): data-devices='{{ devices_json|safe }}' en un atributo de comilla simple — json.dumps no escapa ', así que un nombre de device (editable por el tenant o venido de SNMP) podía cerrar el atributo e inyectar HTML.
  5. Panel Auto-Provision (static/js/editor/auto_provision_panel.js): pintaba name/ip/vendor/model/confidence de SNMP en innerHTML sin escapar — quedó fuera del barrido previo R5-F3.
  6. asset_ids del portal sin filtro de organización (signage/views.py): Playlist.items es JSON libre; un asset_id de otra org colado en un item filtraba su name/thumbnail/tamaño por la ruta anónima.
  7. Sin tope agregado de subida en el borde anónimo (signage/views.py): el portal aceptaba hasta 2 GB/fichero sin cuota agregada — 30 subidas/min podían llenar el disco con un solo token filtrado.

Causa raíz

Dos patrones distintos, no uno: (a) el patrón “|safe + json.dumps dentro de HTML” reaparece cada vez que una vista sirve datos del tenant a una plantilla sin pasar por json_script — ya se había corregido en páginas internas, pero el portal público y el editor quedaron fuera del barrido; (b) saneos que validan por un campo declarado por el cliente (content_type MIME) en vez de por el efectivo (extensión con la que se sirve el fichero), el mismo tipo de desajuste que ya mordió en otros puntos del proyecto.

Fix aplicado

Commit 5d158046d6c0afbb8cc9748de6b2cd6da9fd4c03 (PR #480):

Nota: el mismo PR incluye un fix no relacionado con seguridad — static/js/base.js tenía dos funciones globales openReportModal, y el hoisting pisaba la del menú con la de “Project Report” (Cannot read properties of undefined). Renombrada a openRestoreReportModal. Si se audita el diff de este commit por seguridad, ese hunk es ruido de otra clase de bug.

Los cambios visuales (portal, editor, Project Report) requieren verificación en pantalla tras el deploy — pendiente en el momento de este registro.

Lecciones

Preventivos futuros

Véase también

Subir