
Proof-of-concept for CVE-2026-40864: JupyterHub XSRF bypass via cross-origin form POST exploiting Sec-Fetch-Mode: no-cors. Includes PoC HTML, root cause analysis, and disclosure timeline.
Sec-Fetch-Mode: no-cors)Sévérité : Modérée
CWE : CWE-352 — Cross-Site Request Forgery (XSRF)
Affecté : jupyterhub 4.1.0 ≤ version < 5.4.5 (corrigé dans la version 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 la version 4.1.0) utilisait l'en-tête de requête Sec-Fetch-Mode pour décider si une requête était de même origine. Elle traitait Sec-Fetch-Mode: no-cors comme une requête de même origine — mais no-cors est exactement ce qu'un navigateur envoie pour une . Par conséquent, les requêtes POST cross-origin envoyées via un formulaire HTML vers les points de terminaison de formulaire du Hub (, ) contournaient entièrement le contrôle XSRF.
/hub/spawn/hub/accept-shareL'API JSON n'est pas affectée (elle nécessite un type de contenu non simple, ce qui force une pré-vérification CORS). Seuls les points de terminaison de formulaire HTML sont accessibles de cette manière.
La logique XSRF court-circuite en considérant comme "de confiance" un ensemble d'états Sec-Fetch-* censés représenter des navigations de même origine. Sec-Fetch-Mode: no-cors faisait partie de 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 — exactement le vecteur CSRF classique que le jeton est censé bloquer. Ainsi, tout point de terminaison modifiant l'état qui accepte un corps de formulaire simple et se fie uniquement à cette porte XSRF est forgeable cross-origin.
Transposé aux points de terminaison réels : /hub/spawn (démarrer le serveur de la victime) et /hub/accept-share (forcer la victime à accepter un partage du serveur de l'attaquant) sont tous deux des requêtes POST via formulaire derrière cette porte.
/hub/spawn — une page attaquante peut lancer le serveur mono-utilisateur de la victime sans son 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 de JupyterHub autorisé à partager son propre serveur, il peut forcer une victime à accepter un partage, donnant ainsi à la victime l'accès au serveur de l'attaquant (une étape préparatoire pour des scénarios d'ingénierie sociale / dépôt de données).Utiliser Sec-Fetch-Mode comme oracle d'origine n'est pas fiable : no-cors n'implique pas la même origine. Le correctif dans la version 5.4.5 cesse de considérer no-cors comme 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 proxy inverse.
poc/csrf_spawn.html — hébergez-le sur n'importe quelle origine attaquante et faites-le ouvrir par un utilisateur connecté à JupyterHub. Le formulaire à soumission automatique envoie une requête 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 lancent le serveur de la victime. Pointez action vers /hub/accept-share pour la variante d'acceptation de partage.
1. Modifiez TARGET-JUPYTERHUB dans poc/csrf_spawn.html
2. Servez le fichier depuis une origine attaquante (n'importe quel hôte statique)
3. Ouvrez-le dans un navigateur déjà authentifié auprès du Hub cible
4. Observez le lancement du serveur de la victime sans qu'aucun jeton XSRF ne soit fourni
Divulgué de manière responsable. La preuve de concept a été publiée après la correction.