Mega-auditoría, ronda 6 (25-09-2026): siete brechas cerradas en control de acceso, sesión y CSRF
Cuándo
25-09-2026, PR #609, commit 148961a17de1fac2a587788accb4bc2eba2c61a0 (release v1.161.0, ronda 6 de la mega-auditoría del 24-09-2026). Integra las ramas edu/mega-r6-plataforma (tanda T24) y edu/mega-r6-csrf, cada una revisada por su propio refutador antes de mezclarse.
Síntomas visibles (siete hallazgos, cada uno con su ID de la auditoría)
- B-41 · RLS del Agente: una petición del Agente local (JWT, sin sesión de navegador) llegaba al middleware de aislamiento multi-tenant como anónima y fijaba
app.current_org_id = '0'— sin filtro. Un.filter(org=...)olvidado en un handler que sirve credenciales descifradas enseñaría filas de cualquier organización, no solo la suya. - B-25 · Catálogo de fabricantes: crear/editar fabricantes, subir/borrar ficheros MIB, aplicar OIDs y el asistente de MIBs solo pedían el permiso
network:admin, que cualquier administrador de un cliente tiene. Ese catálogo es compartido por TODAS las organizaciones — un admin de un cliente podía editar el diccionario SNMP que ven todos los demás. - B-38 · Cierre de sesión por inactividad: el temporizador de inactividad (
session_timeout_minutesde cada organización) solo lo aplicaba el navegador (session_timeout.js). Cerrar la pestaña, apagar el equipo o copiar la cookie dejaba la sesión válida en el servidor hasta los 14 días por defecto de Django. - B-43 · Enumeración de usuarios por passkey: el endpoint público de opciones de passkey devolvía las credenciales guardadas cuando se le pasaba
?username=, y nada cuando el usuario no existía o no tenía llave — un oráculo para averiguar qué cuentas existen. - B-UV-04 · Permisos a medida en fallo abierto: si la consulta de un permiso a medida (
ModulePermission) fallaba,has_permissioncaía a los permisos del ROL, que pueden ser más amplios que el permiso a medida que precisamente lo restringía. Un admin rebajado a “solo lectura” en una app volvía a tener permiso de escritura mientras la consulta fallara. - B-36 · Sin CSRF en peticiones de sesión: el piso global de la API (funciones propias de autenticación, no la
SessionAuthde Django Ninja) aceptaba POST/PUT/PATCH/DELETE de una sesión de navegador sin pedir token CSRF; la única barrera eraSameSite=Laxde la cookie. - B-UV-03 · WebSocket sin validar origen: ningún canal WebSocket miraba la cabecera
Origin. El canal de monitorización del navegador solo estaba protegido porSameSite=Lax, que sí viaja desde un subdominio hermano.
Causa raíz común
En los siete casos, el control de acceso asumía un único canal (sesión de navegador del mismo origen) y no cerraba el caso del canal alternativo: el Agente autenticado por JWT, un endpoint público sin usuario todavía identificado, o un fallo de base de datos a mitad de una comprobación de permisos. La brecha aparecía en el hueco entre “camino feliz cubierto” y “qué pasa si esto falla o si quien llama no es un navegador”.
Fix aplicado
core/middleware/tenant_rls.py: con un JWT de acceso válido y sin sesión, el Agente queda acotado a la organización del propio token (terminal/api/auth.py::agent_tenant_for_rls), no a “sin filtro”.network/api/vendor.py: crear/editar fabricantes, subir/borrar MIBs y el asistente de MIBs exigen superusuario además denetwork:admin; el admin de cliente conserva la lectura.core/middleware/session_idle.py(nuevo): al final de cada petición con sesión,set_expiry(session_timeout_minutes * 60). Las pantallas fijas de monitorización (Observatory, Wireless, UPS, Signage) llevandata-session-keepalivey laten al servidor en vez de cerrarse solas.core/auth_api.py:passkey_login_optionssiempre responde en modo credenciales descubribles; ya no busca el?username=.core/utils/permissions.py: si falla la lectura deModulePermission,has_permissiondeniega y lo registra en el log, en vez de caer a los permisos del rol.core/utils/csrf.py(nuevo) +config/urls.py:enforce_session_csrfpasa las peticiones de sesión que cambian datos por elCsrfViewMiddlewarede Django (víaninja.utils.check_csrf); el Agente (JWT) y las rutasauth=Noneno cambian.config/asgi.py: las rutas WebSocket del navegador pasan porAllowedHostsOriginValidator; la ruta del Agente queda fuera a propósito porque su cliente aiohttp no mandaOrigin.
Lecciones
- Un middleware de seguridad que resuelve “usuario autenticado → X, si no → 0/anónimo” necesita un tercer caso explícito para cualquier canal que no sea sesión de navegador (JWT, API key, service token); tratarlo como “igual que anónimo” puede significar “sin filtro” en vez de “sin acceso”.
- Un
except Exception: log and continueen una comprobación de permisos es fail-open por defecto salvo que el código de después deniegue explícitamente — hay que revisar qué pasa si el bloque protegido nunca se ejecuta. - La cabecera
Originy el CSRF token son controles independientes deSameSite; un canal servido desde un subdominio hermano (o un WebSocket) no está cubierto solo por la política de cookies.
Preventivos futuros
Los siete fixes llevan test dedicado (tests/api/test_mega25_r6plataforma_*, tests/api/test_mega25_r6csrf_*) que fija el comportamiento nuevo; la reconciliación entre ramas (fallos de integración al juntar edu/mega-r6-*) quedó en un commit aparte del mismo PR. El detalle completo de la ronda, incluida la parte de rendimiento e infraestructura no cubierta aquí, vive en supercontext/mega-auditoria-2026-09/ (README de estado y hallazgos por tanda).
Véase también
- [[incident—20260924—mega-auditoria-t02-usuarios-privilegios-y-rastro]]
- [[incident—20260905—auditoria-suprema-2-cola-frontend-csp-csrf-escapes]]