CreaRack-SL

s121: Digital Signage — permisos por rol y cierre de 4 raíces de seguridad

Resumen

Sesión 121 (2026-06-09) cierra el módulo de Digital Signage CMS (cartelería digital) con un conjunto completo de parches de seguridad derivados de la Auditoría Suprema dominio signage (commit ae4140b). Implementa el modelo de permisos por rol que ya se aplicaba en racks, networking, monitorización y ITSM, y remedia 4 raíces de vulnerabilidad de alto impacto:

  1. Broken Access Control sistemático — 0 ocurrencias de require_perm en todo signage/ y monitoring/api/signage/
  2. XSS / SVG injection — múltiples vectores (svg_composer, proof_of_play HTML, SVG subido)
  3. Deploy: fuga de credenciales WebDAV + SSRF a infra interna
  4. IDOR y defectos menores — cross-proyecto de assets, upload_corporate_image, N+1

El cambio es transversal a TODO el CMS: mutaciones (content/playlists/players/schedules/share-links/approvals/deployments/display-control) exigen require_perm(signage, edit); lecturas sensibles (reports, deep-data) exigen require_perm(signage, view).


1. Broken Access Control Sistemático (Raíz 1)

Problema

El dominio signage no tenía gate de autorización alguno en los endpoints internos. Todos se basaban únicamente en get_current_org(request) (la organización del usuario autenticado), lo que significaba que:

  • Un usuario readonly (rol con permiso mínimo) podía:
    • ✅ Subir, borrar, reedicar media assets
    • ✅ Crear/borrar playlists, players, schedules
    • ✅ Generar y revocar share-links públicos (exponer contenido a clientes externos)
    • ✅ Aprobar cambios de clientes remotos
    • ✅ Lanzar deploys reales a dispositivos
    • ✅ Controlar pantallas (Samsung MDC, LG, PJLink)

Solución

Registrar y aplicar el scope signage en el sistema de permisos de roles (core/utils/permissions.py):

  • admin: signage: admin
  • operator: signage: edit
  • readonly: signage: view

Puertas cerradas:

  1. signage/api/content.py: mutaciones (upload_media, create_url_asset, update/delete/bulk_delete, reprocess, transcode_status, regenerate_thumbnail) exigen require_perm(signage, edit).

  2. signage/api/playlists.py, players.py, schedules.py: CRUD exige require_perm(signage, edit).

  3. signage/api/share_links.py: crear/borrar ClientShareLink exige require_perm(signage, edit) (un readonly ya no genera links públicos).

  4. signage/api/approvals.py: aplicar cambios pendientes del portal exige require_perm(signage, edit).

  5. signage/api/deployments.py: crear deployment (create_deployment) exige require_perm(signage, edit).

  6. signage/api/display_control.py: todas las mutaciones (power, input, brightness) exigen require_perm(signage, edit).

  7. signage/api/proof_of_play.py: listados sensibles (deep-data con SNMP reales) exigen require_perm(signage, view).

  8. monitoring/api/signage/deploy.py: deploy_content_to_device, restore_operation, upload_corporate_image exigen require_perm(signage, edit).

  9. monitoring/api/signage/dashboard.py, reports.py, summary.py, discovery.py, groups.py: endpoints que leen deep-data o generan reports exigen require_perm(signage, view).

Impacto

  • Un readonly del CMS ya no puede alterar contenido ni lanzar cambios a producción.
  • El permiso manda (modelo consistente con el resto del producto).
  • Las lecturas sensibles (reports SNMP) quedan protegidas de enumeración/exfiltración por usuarios sin rol explícito.

2. XSS y SVG Injection (Raíz 2)

Problema

El módulo generaba y servía SVG dinámico e interpolaba contenido HTML sin sanear, creando 3 vectores de XSS:

2a. svg_composer.py (444 LOC) — atributos de usuario interpolados crudos en SVG:

  • font_family: "Arial" onload="alert(1)" → inyección de atributo
  • color, bg_color, font_weight, text_anchor: escapada insuficiente
  • El SVG se servía como image/svg+xml (inline, ejecuta scripts)

2b. proof_of_play/reporter._to_html — nombres de assets/playlists sin escapar en HTML:

  • Un asset subido con nombre <img src=x onerror=alert(1)> se interpolaba directamente en celdas de tabla
  • Servido como text/html → XSS almacenado

2c. SVG/HTML/Zip subido por cliente anónimo — activos controlados por usuario sin validación de contenido:

  • Un atacante sube un SVG con <script> embebido en el portal público
  • El SVG se sirve después a otros usuarios → XSS cross-tenant

Solución

  1. svg_composer.py: escapar o validar contra patrón estricto:

    • _attr(value): HTML-escape de atributos
    • _safe_color(hex): validar contra patrón ^#[0-9a-f]{6}$
    • _safe_anchor(value): whitelist de start | middle | end
    • _safe_weight(num): validar en rango 100–900
  2. proof_of_play/reporter._to_html: usar html.escape() en todas las celdas de entrada de usuario.

  3. file_storage.save_upload: saneo defensivo de SVG cargado (defensa en profundidad):

    • Neutralizar <script>, on*= (event handlers), javascript: URIs, <foreignObject> (XML embebido)
    • Best-effort contra polyglot maliciosos (SVG + JS embebido difícil de sanear al 100 %)

Impacto

  • Cierra XSS stored en reportes
  • Reduce XSS en SVG dinámico (corporativo)
  • Mitiga carga de activos maliciosos vía portal anónimo

3. Deploy: Credenciales WebDAV y SSRF a Infra Interna (Raíz 3)

Problema

3a. Fuga de credenciales WebDAV al navegador (monitoring/api/signage/deploy.py):

  • deploy_content_to_device leía credenciales HTTP (username/password) del Credential Store global de la org
  • Las credenciales viajaban en claro en la respuesta JSON al browser (task.credentials)
  • Un usuario podía descubrir secretos que ni conocía (credenciales guardadas previamente por admin)

3b. SSRF a infra interna sin validación:

  • El deploy abre conexión WebDAV a la IP del device (device.device_ip)
  • Control de display (Samsung MDC, LG, PJLink) abre sockets TCP a esa IP
  • Adapters de vendor (SpinetiX, BrightSign, etc.) hacen requests HTTP a URLs del vendor/device
  • Ninguno validaba is_private_ip: un dispositivo apuntado a 169.254.169.254 (AWS metadata), 127.0.0.1, o una IP de infra interna (PgBouncer, VictoriaMetrics) forzaría conexiones del servidor desde dentro de la VPC.

Solución

  1. No enviar credenciales al navegador:

    • deploy_content_to_device ahora solo acepta credenciales que el usuario aporta en la petición (si la usa)
    • El Credential Store global se retira de este flujo (backlog completo: agent dispatch vía WebSocket sin pasar credenciales)
  2. SSRF guard en 3 chokepoints (check_scan_ip de network sa1):

    • deploy_content_to_device: valida device.device_ip
    • display_control.controller: valida IP antes de abrir socket TCP
    • adapters.registry.get_adapter: valida antes de hacer requests HTTP a vendor/device

    Política:

    • ✅ RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) — legítimo (on-prem del cliente)
    • ✅ CGNAT (100.64.0.0/10) — legítimo (on-prem del cliente)
    • ❌ Loopback (127.0.0.0/8) — bloquea SSRF a sí mismo
    • ❌ Link-local (169.254.0.0/16) — bloquea AWS metadata (169.254.169.254)
    • ❌ Multicast, broadcast, reserved

Impacto

  • Elimina fuga de credenciales al cliente web
  • Mitiga SSRF a servicios internos de la SaaS (metadata cloud, monitoring, base de datos)
  • Permite on-prem legítimo (clientes con dispositivos en red local)

4. IDOR y Defectos Menores (Raíz 4)

4a. IDOR: Cross-proyecto de assets en portal (M2)

Problema: portal_replace_asset validaba el asset nuevo solo por organización — un cliente externo podía colocar cualquier asset de su org (incl. de otros clientes/proyectos) en su playlist.

Solución: El asset debe estar en el scope del ClientShareLink:

  • Activos ya referenciados en sus playlists permitidas
  • Activos que el propio cliente subió por el portal (nuevo helper _link_allowed_asset_ids)

4b. Revalidación de scope al aprobar cambios del portal (M4)

Problema: approvals._apply_payload aplicaba cambios (playlist_id/schedule_id) sin comprobar que seguían dentro del scope del ClientShareLink original (el scope pudo cambiar entre petición y aprobación).

Solución: _apply_payload ahora valida que el playlist/schedule del cambio sigue en el scope del link antes de aplicar.

4c. IDOR en create_deployment (M8)

Problema: deployments.py validaba los player_ids por organización pero persistía playlist_id/schedule_id crudos sin validar que pertenecieran a la org.

Solución: Rechaza (400) un playlist/schedule que no pertenezca a la org antes de crear el ContentDeployment.

4d. upload_corporate_image borra antes de validar (B5)

Problema: El endpoint borraba TODAS las corporate assets de la org antes de validar/guardar el fichero nuevo — un upload inválido dejaba a la org sin imagen corporativa.

Solución: Guardar y validar primero, borrar después (comportamiento idempotente).

4e. N+1 en list_operations (B4)

Problema: monitoring/api/signage/dashboard.py:list_operations cargaba la tabla entera de playlists de la org para resolver nombres de playlist en las operaciones.

Solución: Solo consultar los playlist_id presentes en las 50 operaciones mostradas (paginación por defecto).


Cambios Técnicos Clave

ComponenteCambioEfecto
core/utils/permissions.pyAñadir scope signage (admin/edit/view)Todos los gates ahora reconocen el módulo
signage/api/*.pyDecorador @require_perm(signage, edit/view) en endpoints internosMutaciones bloqueadas a readonly
monitoring/api/signage/*.pyÍdem + gates en deploy/dashboard/reportsDeploy bloqueado a readonly
svg_composer.pyEscapar atributos; validar colores/anchorsCierra XSS en SVG dinámico
proof_of_play/reporter.pyhtml.escape() en celdas HTMLCierra XSS stored en HTML
file_storage.pySaneo defensivo de SVG subidoMitiga carga maliciosa
deploy.pyQuitar lectura de Credential Store; aplicar check_scan_ipEvita fuga de creds + SSRF
display_control/Aplicar check_scan_ip en sockets TCPMitiga SSRF local
adapters/Aplicar check_scan_ip en requests HTTPMitiga SSRF a vendors
portal.pyValidar scope en portal_replace_asset + _can_editMitiga IDOR fuera de scope
approvals.pyRevalidar scope en _apply_payloadScope no se mueve entre petición y aprobación
deployments.pyValidar org de playlist/scheduleRechaza IDOR en deployment

Entrada en RELEASE_NOTES (s121)

El módulo de cartelería digital (Digital Signage) ahora respeta los roles de usuario como el resto del producto. Hasta ahora cualquier persona con acceso al módulo —incluido un usuario de solo lectura— podía hacer de todo: subir y borrar contenido, crear y eliminar listas de reproducción y pantallas, generar enlaces públicos para clientes externos, aprobar cambios e incluso lanzar despliegues a los dispositivos. A partir de ahora, solo quien tiene permiso de edición puede hacer esos cambios; los usuarios de solo lectura pueden ver pero no tocar. Es el mismo modelo de permisos que ya aplicábamos en racks, redes y monitorización. Sale de la auditoría de seguridad del dominio signage (parte de la Auditoría Suprema).


Pruebas Recomendadas

  1. Authz (require_perm):

    • readonly no puede POST/PUT/DELETE en signage/* endpoints
    • readonly no puede lanzar deploy, restaurar operaciones
    • readonly CAN GET listados y reportes (si tienen signage: view)
  2. Portal público (ClientShareLink):

    • portal_replace_asset: asset no permitido → 403 (antes 200)
    • portal_update_playlist: playlist fuera del scope → 403
    • Scope no se ensancha entre petición y aprobación
  3. SSRF:

    • device_ip = 127.0.0.1 → deploy rechazado (antes podría intentarse)
    • device_ip = 169.254.169.254 → rechazado
    • device_ip = 192.168.1.1 → permitido (on-prem)
  4. XSS / SVG:

    • SVG con <script> subido → neutralizado (sin <script> en respuesta)
    • Proof-of-play HTML con asset malicioso → escapado
    • svg_composer con font_family="Arial" onload=…" → escapado
  5. IDOR:

    • corporate_image subido inválido → no se borran assets previos
    • list_operations no genera N+1 (verificar con django-debug-toolbar)

Linaje y Contexto

Esta sesión es parte de la Auditoría Suprema 2026 (iniciativa mayor de hardening de dominio en dominio). Las raíces remediadas aquí se han confirmado reales con verificación adversarial en 3 lentes (precisión de código, impacto real, novedad/validez) para ALTA y MEDIA.

El patrón de Broken Access Control (clave 1) es el mismo que se identificó y se cerró en 5 dominios previos (network sa1–sa5, monitoring, core) — es válido reportarlo como hallazgo NUEVO en signage porque el módulo sí carecía de gates.

Véase también

  • [[concept—saas—multi-tenancy]]
  • [[entity—core—model—organization]]
  • [[concept—seguridad—broken-access-control]]
  • [[feature—monitoring—require-perm-network]]
  • [[entity—signage—model—clientsharelink]]