
Preuve de concept pour CVE-2026-40864 : contournement XSRF de JupyterHub via une requête POST de formulaire cross-origin exploitant Sec-Fetch-Mode: no-cors. Comprend un fichier HTML de PoC, une analyse de la cause racine et une chronologie de divulgation.
Sec-Fetch-Mode: no-cors)Gravité : Moyenne (CVSS 5.4)
CWE : CWE-352 — Cross-Site Request Forgery (XSRF)
Affecté : jupyterhub 4.1.0 ≤ version < 5.4.5 (corrigé dans 5.4.5)
Avis : GHSA-m68r-v472-jgq9
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-40864
Crédit : Romain Deperne
La protection XSRF de JupyterHub (remaniée dans 4.1.0) utilisait l'en-tête de requête Sec-Fetch-Mode pour déterminer
si une requête était de même origine. Elle traitait Sec-Fetch-Mode: no-cors comme étant de même origine — mais
no-cors est exactement ce qu'un navigateur envoie pour une soumission de formulaire « simple » cross-origin. En
conséquence, les POST de formulaires HTML cross-origin vers les endpoints de formulaire du Hub (/hub/spawn, /hub/accept-share)
contournaient entièrement la vérification XSRF.
L'API JSON n'est pas affectée (elle exige un type de contenu non simple, ce qui force une pré-vérification CORS). Seuls les endpoints de formulaire HTML sont atteignables de cette manière.
La logique XSRF court-circuite vers « de confiance » pour un ensemble d'états Sec-Fetch-* censés capturer
les navigations de même origine. Sec-Fetch-Mode: no-cors était inclus dans cet ensemble de confiance. Mais no-cors
est le mode que le navigateur attribue à un simple <form method=POST> postant vers une origine différente —
précisément le vecteur CSRF classique que le jeton est censé bloquer. Ainsi, tout endpoint modifiant l'état qui
accepte un corps de formulaire simple, et repose uniquement sur cette barrière XSRF, est forgeable en cross-origin.
En appliquant cela aux endpoints réels : /hub/spawn (démarrer le serveur de la victime) et /hub/accept-share
(amener la victime à accepter un partage du serveur de l'attaquant) sont tous deux des POST de formulaire derrière cette barrière.
/hub/spawn — une page d'attaquant peut démarrer le serveur mono-utilisateur de la victime sans consentement
(consommation de ressources / état inattendu ; l'attaquant n'obtient pas l'accès à ce serveur)./hub/accept-share — lorsque l'attaquant est un utilisateur JupyterHub autorisé à partager son propre
serveur, il peut forcer une victime à accepter un partage, donnant à la victime l'accès au serveur de
l'attaquant (une étape de préparation pour d'autres scénarios d'ingénierie sociale / de dépôt de données).Utiliser Sec-Fetch-Mode comme oracle d'origine n'est pas fiable : no-cors n'implique pas le même origine. Le correctif
dans 5.4.5 cesse de considérer no-cors comme étant de même origine. Les opérateurs qui ne peuvent pas mettre à jour immédiatement peuvent rejeter
les requêtes portant Sec-Fetch-Mode: no-cors au niveau du reverse proxy.
poc/csrf_spawn.html — hébergez-le sur n'importe quelle origine d'attaquant et faites-le ouvrir par un utilisateur JupyterHub connecté.
Le formulaire à soumission automatique émet un POST cross-origin vers /hub/spawn (le navigateur envoie
Sec-Fetch-Mode: no-cors) ; les versions vulnérables l'acceptent sans jeton _xsrf valide et démarrent le
serveur de la victime. Pointez action vers /hub/accept-share pour la variante d'acceptation de partage.
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
Divulgué de manière responsable. PoC publié après la livraison du correctif.