
JupyterHub XSRF bypass mediante cross-origin form POST (Sec-Fetch-Mode: no-cors) — CWE-352
Sec-Fetch-Mode: no-cors)Gravedad: Moderada
CWE: CWE-352 — Falsificación de solicitudes entre sitios (XSRF)
Afecta: jupyterhub 4.1.0 ≤ versión < 5.4.5 (corregido en 5.4.5)
Aviso: GHSA-m68r-v472-jgq9
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40864
Crédito: Romain Deperne
La protección XSRF de JupyterHub (rediseñada en 4.1.0) usaba la cabecera de solicitud Sec-Fetch-Mode para decidir si una solicitud era del mismo origen. Trataba Sec-Fetch-Mode: no-cors como mismo origen, pero no-cors es exactamente lo que un navegador envía para un envío de formulario "simple" entre orígenes. Como resultado, los POST de formularios HTML entre orígenes a los endpoints de formulario del Hub (/hub/spawn, /hub/accept-share) omitían por completo la comprobación XSRF.
La API JSON no se ve afectada (requiere un tipo de contenido no simple, lo que obliga a un preflight CORS). Solo los endpoints de formulario HTML son alcanzables de esta manera.
La lógica XSRF se cortocircuita a "confiable" para un conjunto de estados Sec-Fetch-* destinados a capturar navegaciones del mismo origen. Sec-Fetch-Mode: no-cors estaba incluido en ese conjunto confiable. Pero no-cors es el modo que el navegador asigna a un <form method=POST> simple que se envía a un origen diferente — precisamente el vector clásico de CSRF que el token debe detener. Por lo tanto, cualquier endpoint que cambie el estado, que acepte un cuerpo de formulario simple y que dependa únicamente de esta compuerta XSRF, es falsificable entre orígenes.
Al mapear eso a endpoints reales: /hub/spawn (iniciar el servidor de la víctima) y /hub/accept-share (hacer que la víctima acepte un recurso compartido del servidor del atacante) son ambos POST de formulario detrás de esta compuerta.
/hub/spawn — una página del atacante puede iniciar el servidor de usuario único de la víctima sin su consentimiento (consumo de recursos / estado inesperado; el atacante no obtiene acceso a ese servidor)./hub/accept-share — cuando el atacante es un usuario de JupyterHub con permiso para compartir su propio servidor, puede forzar a una víctima a aceptar un recurso compartido, dando a la víctima acceso al servidor del atacante (un paso preparatorio para escenarios posteriores de ingeniería social / entrega de datos).Usar Sec-Fetch-Mode como oráculo de origen no es sólido: no-cors no implica mismo origen. La corrección en 5.4.5 deja de confiar en no-cors como mismo origen. Los operadores que no puedan actualizar de inmediato pueden descartar las solicitudes que lleven Sec-Fetch-Mode: no-cors en el proxy inverso.
poc/csrf_spawn.html — alójalo en cualquier origen del atacante y haz que un usuario de JupyterHub con la sesión iniciada lo abra. El formulario de autoenvío realiza un POST entre orígenes a /hub/spawn (el navegador envía Sec-Fetch-Mode: no-cors); las versiones vulnerables lo aceptan sin un token _xsrf válido e inician el servidor de la víctima. Apunta action a /hub/accept-share para la variante de aceptación del recurso compartido.
1. Edit TARGET-JUPYTERHUB in poc/csrf_spawn.html
2. Serve the file from an attacker origin (any static host)
3. Open it in a browser already authenticated to the target Hub
4. Observe the victim's server spawn with no XSRF token supplied
Divulgado de forma responsable. El PoC se publicó después de que se distribuyó la corrección.