
Proof-of-concept для CVE-2026-40864: обход XSRF в JupyterHub через межсайтовый POST-запрос формы, эксплуатирующий Sec-Fetch-Mode: no-cors. Включает PoC на HTML, анализ первопричины и хронологию раскрытия.
Sec-Fetch-Mode: no-cors)Уровень серьёзности: Средний (CVSS 5.4)
CWE: CWE-352 — подделка межсайтовых запросов (XSRF)
Затронуто: jupyterhub 4.1.0 ≤ версия < 5.4.5 (исправлено в 5.4.5)
Рекомендация: GHSA-m68r-v472-jgq9
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40864
Автор: Romain Deperne
Защита JupyterHub от XSRF (переработанная в 4.1.0) использовала заголовок запроса Sec-Fetch-Mode, чтобы определить,
является ли запрос одноисточниковым. Она считала Sec-Fetch-Mode: no-cors одноисточниковым — но
no-cors — это именно то, что браузер отправляет при кросс-доменной «простой» отправке формы. В результате
кросс-доменные POST-запросы HTML-форм к эндпоинтам форм Hub (/hub/spawn, /hub/accept-share)
полностью обходили проверку XSRF.
JSON API не затронут (он требует не-простого типа содержимого, что вызывает предварительный CORS-запрос). Таким способом достижимы только эндпоинты HTML-форм.
Логика XSRF сразу помечает как «доверенные» ряд состояний Sec-Fetch-*, предназначенных для
одноисточниковых переходов. Sec-Fetch-Mode: no-cors входил в этот доверенный набор. Но no-cors
— это режим, который браузер назначает обычному <form method=POST>, отправляющему данные на другой источник, —
именно классический вектор CSRF, который токен и должен блокировать. Таким образом, любой эндпоинт, изменяющий состояние,
который принимает простое тело формы и полагается исключительно на этот шлюз XSRF, может быть подделан кросс-доменно.
В применении к реальным эндпоинтам: /hub/spawn (запуск сервера жертвы) и /hub/accept-share
(принуждение жертвы принять общий доступ к серверу атакующего) — оба являются POST-запросами форм за этим шлюзом.
/hub/spawn — страница атакующего может запустить однопользовательский сервер жертвы без согласия
(расход ресурсов / неожиданное состояние; атакующий не получает доступа к этому серверу)./hub/accept-share — если атакующий является пользователем JupyterHub, которому разрешено делиться своим
сервером, он может заставить жертву принять общий доступ, дав жертве доступ к серверу атакующего
(подготовительный шаг для дальнейших сценариев социальной инженерии / подброса данных).Использование Sec-Fetch-Mode в качестве оракула источника некорректно: no-cors не подразумевает одноисточниковость. Исправление
в 5.4.5 перестаёт считать no-cors одноисточниковым. Операторы, которые не могут немедленно обновиться, могут отбрасывать
запросы с Sec-Fetch-Mode: no-cors на обратном прокси.
poc/csrf_spawn.html — разместите его на любом источнике атакующего и попросите вошедшего пользователя JupyterHub открыть его.
Автоматически отправляемая форма выполняет кросс-доменный POST-запрос к /hub/spawn (браузер отправляет
Sec-Fetch-Mode: no-cors); уязвимые сборки принимают его без действительного токена _xsrf и запускают
сервер жертвы. Укажите в action значение /hub/accept-share для варианта с принятием общего доступа.
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
Раскрыто ответственно. PoC опубликован после выпуска исправления.