Volver a la wiki

Cerrar Broken Access Control sistémico en Digital Signage

Contexto y problema

La Auditoría Suprema del dominio Digital Signage (audit-only, .claude/workflows/auditoria-suprema-signage.js) identificó un patrón crítico de seguridad que coincidía en todos los dominios previos auditados (network sa1-sa5, monitoring, racks): Broken Access Control sistémico.

Hallazgo principal

El módulo signage (9.6k LOC repartidas entre signage/ y monitoring/api/signage/) no contenía ni una sola ocurrencia de require_perm. Todos los endpoints internos exigían solo get_current_org(request) (obtener la organización del usuario). Esto significaba:

# Síntoma: un readonly podía hacer ESTO
POST /api/signage/content/upload_media/
→ upload_media(org=user.org) — SOLO filtra por org, no por rol
→ readonly puede subir media ✗

# Se suponía que debería ser
POST /api/signage/content/upload_media/
→ require_perm(request, "signage", "edit") — primero valida permiso
→ si readonly → 403 Forbidden

# Síntoma: readonly generaba links públicos
POST /api/signage/share-links/
→ create_share_link(org=user.org) — SOLO org
→ readonly genera link público a contenido ✗

# Síntoma: readonly disparaba despliegues
POST /api/signage/deployments/
→ create_deployment(org=user.org) — SOLO org
→ readonly dispara deploy a 100 pantallas públicas ✗

Gravedad

Severidad: ALTA — control de acceso es fundamental para la seguridad multi-tenant. Un usuario readonly es una persona con permiso restringido deliberado (auditor, asistente de solo lectura, usuario de cliente externo); que pueda mutar datos viola confianza, cumplimiento normativo (GDPR/SOC2) e integridad de negocio.


Decisión

Cerrar el Broken Access Control aplicando el estándar require_perm en TODOS los endpoints de signage.

El sistema ya disponía del mecanismo desde fases anteriores (core/utils.require_perm(request, scope, action)) tras desplegarse en 5 dominios previos. No es una nueva abstracción; es aplicar lo que ya funciona.

Estándar adoptado

from core.utils import require_perm

# Dentro de cada endpoint que MUTA
def upload_media(request, content_in):
    org = require_perm(request, "signage", "edit").org  # 403 si readonly
    media = MediaAsset.objects.create(organization=org, ...)
    return {"ok": True, "asset_id": media.id}

# Dentro de cada endpoint que LEE datos SENSIBLES (reportes, datos SNMP)
def get_deep_snmp_data(request, player_id):
    org = require_perm(request, "signage", "view").org  # 403 si sin permiso
    player = SignagePlayer.objects.get(pk=player_id, organization=org)
    return {"player": player, "snmp_data": fetch_snmp(player.ip)}

Rol-permission mapping (core/utils/permissions.py):


Cambios implementados (raíces 1-4 de auditoría)

Raíz 1: Broken Access Control en endpoints internos

CategoríaEjemplosFix
Contentupload_media, delete_media, bulk_delete_media, transcode_media, regenerate_thumbnailrequire_perm(signage, edit)
Playlistscreate_playlist, update_playlist, delete_playlist, add_item, remove_itemrequire_perm(signage, edit)
Playerscreate_player, update_player, delete_playerrequire_perm(signage, edit)
Schedulescreate_schedule, update_schedule, delete_schedulerequire_perm(signage, edit)
Share linkscreate_share_link, update_share_link, delete_share_linkrequire_perm(signage, edit)
Approvalsapprove_change, reject_changerequire_perm(signage, edit)
Deploymentscreate_deployment, delete_deployment, trigger_deployrequire_perm(signage, edit)
Display controlsamsung_mdc_command, lg_protocol_command, pjlink_commandrequire_perm(signage, edit)
Deep readsget_deep_snmp_data, list_reports, get_sync_statusrequire_perm(signage, view)

Raíz 2: XSS / SVG injection (secundario a BAC, pero crítico)

Raíz 3: Deploy - credenciales + SSRF

Raíz 4: IDOR / honestidad menores


Validación de scope del portal (M4)

El portal de clientes (ClientShareLink token-based) permite que un cliente externo suba/edite contenido mediante un link con token público y acceso limitado. La auditoría identificó que la validación del scope se hacía al originarse el cambio pero NO al aprobarlo:

# Flujo del portal cliente
1. Cliente externo carga portal vía link (token)
   → _get_valid_link(token) obtiene ClientShareLink
   → scope: allowed_playlists = [1, 2, 3], allowed_players = [A, B, C]

2. Cliente sube media y solicita que se añada a playlist #1 (en scope)
   → approvals.create(playlist_id=1, ...)  ← OK

3. [HORAS DESPUÉS] La scope cambió: allowed_playlists = [] (link revocado)
   → admin aprueba el cambio
   → approvals._apply_payload(playlist_id=1) — NO validaba scope de nuevo
   → media se añade a playlist #1 aunque el link ya no tiene acceso ✗

Fix (M4): _apply_payload ahora revalida que el playlist_id/schedule_id sigue dentro del scope del link original.


Justificación

  1. Consistencia: el estándar ya funciona en network, racks, monitoring. Aplicarlo en signage no introduce nuevo código.
  2. Seguridad multi-tenant: fundamental para SaaS. Un readonly deliberado es una restricción, no una preferencia de UI.
  3. Cumplimiento: SOC2/GDPR exigen control de acceso basado en roles; esto lo implementa.
  4. Bajo coste: require_perm es un guard de 2-3 líneas; ya estaba todo el terreno preparado.
  5. Precedente: 5 dominios previos ya lo tienen. Signage era la excepción.

Riesgos y mitigación

RiesgoMitigación
Scripts/bots esperaban readonly sin gatesCHANGELOG claro en s121; endpoint ahora 403
Admin tiene que otorgar permisos editores a usuarios que antes no teníanDocumentación de migración (RELEASE_NOTES); check en bootstrap de org
Rendimiento: +1 query por endpoint para validar permisorequire_perm usa get_current_org (ya cacheada en request); impacto nulo

Trabajo futuro (backlog)


Véase también

Subir