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:
- Portal público de cliente (
templates/signage/client_portal.html+signage/views.py):devices_json/playlists_jsonse inyectaban con|safedentro 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. - Composer de SVG (
signage/services/svg_composer.py): un fix previo (A4) ya saneaba color/fuente/anchor/weight, perox/y/width/height,duration,opacityyspeedse interpolaban crudos en atributos del SVG servido comoimage/svg+xml— ypositiones una clave whitelisteada que el portal anónimo puede fijar. - 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 elcontent_typedeclarado por el cliente eraimage/svg+xml— pero el fichero se sirve por su extensión, así que bytes SVG declaradosimage/pngcon nombrex.svgse servían con el script vivo. - XSS almacenado en el editor de racks (
templates/editor.html):data-devices='{{ devices_json|safe }}'en un atributo de comilla simple —json.dumpsno escapa', así que un nombre de device (editable por el tenant o venido de SNMP) podía cerrar el atributo e inyectar HTML. - Panel Auto-Provision (
static/js/editor/auto_provision_panel.js): pintaba name/ip/vendor/model/confidence de SNMP eninnerHTMLsin escapar — quedó fuera del barrido previo R5-F3. asset_idsdel portal sin filtro de organización (signage/views.py):Playlist.itemses JSON libre; unasset_idde otra org colado en un item filtraba su name/thumbnail/tamaño por la ruta anónima.- 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):
- Patrón
json_script+JSON.parse(…textContent)en el portal público, eliminando losjson.dumps+|safedesignage/views.py. - Nuevo helper
_num()ensvg_composer.py(coerción a numérico finito, si nodefault) aplicado a todo atributo de geometría,duration,opacityyspeed. file_storage.pysanea por extensión efectiva además de por MIME declarado.media_file(views_publish.py) añadeX-Content-Type-Options: nosniffy sirve.svg/.html/.htm/.zipconContent-Disposition: attachment.- Quitado el
|safededata-deviceseneditor.html(el autoescape de Django cubre'). auto_provision_panel.jsimportaescHtmlde la fuente única y escapa los 5 campos SNMP.signage/views.py:organization=project.organizationen el filtro deasset_ids; tope de 500 MB/fichero vía portal + cuota de 20 GB de media signage por organización.- 15 tests nuevos en
tests/api/test_signage_xss.py.
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
- El patrón
|safe+json.dumpsen<script>inline hay que barrerlo por superficie de exposición (¿anónima? ¿sin CSP?), no solo por página — el portal público quedó fuera de rondas anteriores precisamente por ser la ruta menos visitada en revisión manual. - Saneo que confía en un campo declarado por el cliente (MIME) en vez del efectivo (extensión de servicio) es una clase de bug recurrente en el proyecto — cualquier saneo de contenido subido debe validar contra cómo se sirve, no contra cómo se anuncia.
Preventivos futuros
- Residuales documentados como BAJA (defensa en profundidad): el
innerHTMLdel preview del portal sigue confiando en el saneo server-side del composer; la exención de CSP en/client/se mantiene (retirarla exige nonces y romper estilos inline); el poll de/api/jobs×3 sin reintentos y el i18n del JS inline del portal quedan para la siguiente pasada de MEDIA/BAJA. - Continuar la Tanda 3+ de la Auditoría Suprema 2 sobre los dominios que falten.
Véase también
- [[incident—20260609—audit-suprema-signage]]
- [[decision—20260609—signage-broken-access-control]]
- [[incident—20260823—signage-publisher-cross-tenant-playlist-leak]]
- [[feature—security—auditoria-suprema-2-tanda-1-core]]
- [[incident—20260831—auditoria-suprema-2-tanda1-monitoring-racks]]
- [[entity—signage—model—mediaasset]]
- [[crearack—signage—client-portal]]
Referenciado desde
- Auditoría Suprema 2 · Cola frontend: CSP del portal de cliente, escapes y CSRF same-origin
- Auditoría Suprema 2 · Tanda 6: toma de control del Agente local desde cualquier web y downgrade forzado del .exe
- Auditoría Suprema 2 · Tanda 8 (workspace, última de las 8): el token del doctor autorizaba escritura a todo /api/*
- Auditoría Suprema 2 · Tanda 9: SSRF por IPv6 mapeada, XSS almacenado en stencils y borde anónimo de signage