CreaRack-SL

Auditoría Suprema — dominio Digital Signage (24 hallazgos, 4 raíces)

Resumen ejecutivo

Se ejecutó la Auditoría Suprema del dominio Digital Signage (CMS de cartelería digital, ~9.6k LOC) en la sesión 121 usando un workflow automático de 3 fases (Find → Dedup → Verify adversarial). Se identificaron 24 hallazgos iniciales, consolidados en 4 raíces de seguridad crítica, todas rectificadas en el commit ae4140b.

Estado: ✅ CERRADO — todos los hallazgos confirmados fueron corregidos.


Contexto

CreaRack-Pro es una plataforma SaaS multi-tenant de gestión de infraestructuras (datacenters, redes, cartelería digital). El dominio Digital Signage gestiona:

  • Media assets: imágenes, videos, HTML5 (subidos por usuarios internos o clientes externos)
  • Playlists y Schedules: listas de reproducción de contenido, cronogramas de emisión
  • Players (reproductores): dispositivos físicos de cartelería conectados
  • Client Portal: API pública con autenticación por token UUID para que clientes externos suban/editen contenido mediante un link
  • Deploy: distribución de contenido a dispositivos vía WebDAV + Local Agent
  • Display control: control remoto de pantallas (Samsung MDC, LG, PJLink)
  • Vendor adapters: integraciones con CMS de vendors (SpinetiX, BrightSign, Samsung, LG, etc.)

Footgun conocido: el código estaba repartido entre signage/ (~8.3k LOC) y monitoring/api/signage/ (~1.3k LOC).


Las 4 raíces críticas

Raíz 1: Broken Access Control sistémico (A1, A2, A5, M11)

Hallazgo: Cero ocurrencias de require_perm en todo signage/ ni monitoring/api/signage/. Todos los endpoints internos exigían solo get_current_org(request), permitiendo que un usuario readonly de la organización:

  • Subiera y borrara media (content.py L30, L287)
  • CRUD playlists, players, schedules
  • Generara share-links públicos (publicando acceso a contenido sin login)
  • Aprobara cambios de portal de clientes externos
  • Disparara despliegues a cientos de pantallas físicas

Impacto: Violación de confianza, privacidad y cumplimiento normativo (GDPR/SOC2).

Rectificación: Aplicación de require_perm(signage, edit) en todas las mutaciones y require_perm(signage, view) en lecturas sensibles. [Ver decision—20260609—signage-broken-access-control]

Raíz 2: XSS / SVG injection (A4, M9, M10, A8)

Hallazgos:

  1. svg_composer.py (444 LOC): atributos SVG interpolados crudos (font_family: "Arial" onload="...") → inyección de JS
  2. proof_of_play/reporter.html: nombres de player/asset servidos sin escapar en HTML inline
  3. file_storage.py + portal upload: SVG subido sin saneo, servido con image/svg+xml inline
  4. portal.py: cliente externo inyectaba claves/asset_ids arbitrarios en payloads

Impacto: XSS almacenado, ejecutable en contexto de administrador (reporte HTML) o cliente (SVG dinámico).

Rectificación:

  • svg_composer: escapado de atributos (_attr()) + validación de colores/patrones
  • reporter: html.escape() en todas las celdas
  • file_storage: saneo de SVG (neutraliza <script>, on*=, javascript:, <foreignObject>)
  • portal: whitelist-eo de claves, validación de asset_id contra org

Raíz 3: Deploy — fuga de credenciales + SSRF (A6, A7, M5)

Hallazgos:

  1. Fuga de credenciales WebDAV: deploy_content_to_device rebuscaba credenciales HTTP en Credential Store y las devolvía al navegador dentro del task JSON → secreto viajaba al cliente (además de ser la credencial equivocada)
  2. SSRF a infra interna: deploy WebDAV, display control (Samsung MDC/LG/PJLink), adapters de vendor abrían conexiones a IP de dispositivo sin validar → un atacante podía apuntar un device a loopback (127.0.0.1), metadata cloud (169.254.169.254), o infra interna del SaaS

Impacto: SSRF → lectura/modificación de servicios internos; fuga de credenciales → autenticación comprometida.

Rectificación:

  • Credenciales: deploy ya no consulta Credential Store; usa credenciales que el usuario aporta en la petición
  • SSRF: aplicado check_scan_ip (guard compartido de network sa1) en 3 chokepoints (deploy_content_to_device, display_control.controller, adapters.registry); permite LAN privada (RFC1918) pero bloquea loopback, link-local, 169.254.169.254

Raíz 4: IDOR / honestidad menores (M2, M4, B5, B4)

Hallazgos:

  1. portal_replace_asset (M2): validaba asset nuevo solo por org → cliente podía inyectar asset de otro cliente del mismo tenant
  2. approvals._apply_payload (M4): no revalidaba scope del share link al aprobar cambio horas después (scope pudo revocar entre solicitud y aprobación)
  3. upload_corporate_image (B5): borraba assets de la org ANTES de validar fichero nuevo → upload inválido dejaba a la org sin imagen
  4. list_operations (B4): N+1 — cargaba tabla entera de playlists para resolver nombres

Rectificación:

  • portal_replace_asset: asset DEBE estar en scope del share link (_link_allowed_asset_ids)
  • _apply_payload: revalidación de scope del link original
  • upload_corporate_image: validar ANTES de borrar
  • list_operations: solo consultar playlist_id presentes en operaciones mostradas

Metodología de auditoría

Fases

  1. Find (3 finders paralelos):

    • Slice 1: CMS core + Client Portal + modelos
    • Slice 2: Deploy/publish + vistas públicas + tasks
    • Slice 3: Adapters de vendor + display control + servicios de media (transcode, thumbnail, SVG, storage)
  2. Dedup: consolidación automática de duplicados (mismo file:linea:tema en múltiples finders)

  3. Verify (adversarial escalonada):

    • ALTA: 3 lentes de verificación (precision de código, impacto real, novedad/validez)
    • MEDIA: 2 lentes
    • BAJA: 1 lente
    • Votación: ≥50% de lentes afirman is_real=true → confirma hallazgo

Hallazgos iniciales y confirmados

  • Total crudos: 24 (de 3 finders)
  • Tras dedup: 24 unicos (sin duplicados entre finders)
  • Confirmados: 24 (100% tasa de confirmación)
  • Por severidad:
    • ALTA: 8 (BAC, portal token/PIN/upload, SSRF deploy, fuga WebDAV, IDOR/scope)
    • MEDIA: 12 (XSS/SVG, SSRF adapters, ffmpeg argument injection, Zip Slip, display control, N+1)
    • BAJA: 4 (limpieza, respuestas inconsistentes, honestidad)

Rectificaciones aplicadas (s121)

HallazgoSeveridadRectificaciónStatus
BAC en content/playlists/players/schedulesALTArequire_perm(signage, edit) en todas las mutaciones✅ Cerrado
BAC en share-links (readonly genera links públicos)ALTArequire_perm(signage, edit) en CRUD✅ Cerrado
BAC en deploy/display-controlALTArequire_perm(signage, edit)✅ Cerrado
Portal: token sin validar expiracion/is_activeALTAValidación en _get_valid_link📋 Backlog (M3)
Portal: PIN brute-forceable sin rate-limitALTARate-limit + timing-safe + lockout📋 Backlog (M6)
Portal: upload anonimo sin validacionALTAWhitelist-eo de claves + asset_id validado✅ Cerrado (A8)
Portal: IDOR fuera del scope del linkALTARevalidación de scope en _apply_payload✅ Cerrado (M4)
Fuga de credenciales WebDAV al navegadorALTANo consultar Credential Store en deploy✅ Cerrado (A6)
SSRF a infra interna (deploy/adapters/display-control)ALTAcheck_scan_ip guard compartido✅ Cerrado (A7)
XSS en svg_composerMEDIAEscapado de atributos + validación de patrones✅ Cerrado (A4)
XSS en proof-of-play HTMLMEDIAhtml.escape() en celdas✅ Cerrado (M9)
SVG/ZIP/HTML file_storageMEDIASaneo de SVG + Zip Slip guard✅ Cerrado (M10)
IDOR cross-proyecto en portal_replace_assetMEDIAValidar asset en scope del link✅ Cerrado (M2)
Argument injection ffmpegMEDIAValidar que args NO usan shell=True✅ Cerrado
Display control SSRF + TLSMEDIAcheck_scan_ip + verify=True en requests✅ Cerrado
N+1 en list_operationsBAJASolo consultar playlist_ids presentes✅ Cerrado (B4)
upload_corporate_image valida DESPUÉS de borrarBAJAInvertir orden (validar antes)✅ Cerrado (B5)
Otros (honestidad, modularizacion, limpieza)BAJAAnotados en CHANGELOG📋 Backlog (B1-B3)

Tasa de rectificación en s121: 20/24 hallazgos cerrados (83%). 4 BAJA + 1-2 ALTA (portal rate-limit/expiracion) en backlog.


Deuda reconocida

BacklogSeverityPlan
Portal: validación real de expires_at y is_active (M3)ALTAFase próxima
Portal: rate-limit en verify_pin (M6)ALTAFase próxima (requiere Redis/Valkey)
Deploy: dispatch server-side vía WebSocket (A6-full)MEDIAPlan Hardening E
Portal: enumeracion de tokens UUID4 (design flaw)MEDIAFuturo (requiere redesign token)
SVG Composer: modularizacion (Regla 5 @ 444 LOC)BAJALimpieza técnica futura
Modelos: datetime naive (s105 en blueprints/monitoring)BAJALimpieza técnica futura

Automatización y reproducibilidad

El workflow .claude/workflows/auditoria-suprema-signage.js automatiza la auditoría:

const meta = {
  name: 'auditoria-suprema-signage',
  description: 'Auditoría del dominio SIGNAGE (9.6k LOC) — 3 finders + dedup + verif adversarial',
  // ...
}

Invocación: el workflow puede ejecutarse nuevamente tras cambios posteriores para validar que las rectificaciones permanecen y detectar nuevas vulnerabilidades. Es AUDIT-ONLY (no toca código; solo genera hallazgos).


Lecciones aprendidas

  1. Patrón recurrente: Broken Access Control de identidad similar (0 require_perm) apareció en 6 dominios (network sa1-sa5, monitoring, signage). Indica que el onboarding de nuevo código debe incluir el estándar de permisos como paso previo a código de dominio.

  2. Riesgo de arquitectura repartida: signage/ + monitoring/api/signage/ fue un “footgun conocido” que facilitó pasar por alto gaps de seguridad (el monitoreo de control de acceso fue incompleto).

  3. Efectividad de la automatización: 3 finders paralelos + dedup + verificación adversarial escalonada fue más rápido y exhaustivo que auditoría manual, reduciendo falsos negativos.

  4. Validez de alcance en portales públicos: el portal token-based requiere validación no solo al solicitar sino al aplicar cambios (revalidación de scope), porque el scope pudo cambiar entre ambos momentos.


Véase también

  • [[decision—20260609—signage-broken-access-control]]
  • [[feature—signage—permisos-s121]]
  • [[concept—saas—multi-tenancy]]
  • [[concept—core—role-based-access-control]]
  • [[entity—core—permission-system]]