
Обход XSRF в JupyterHub через межсайтовую POST-форму (Sec-Fetch-Mode: no-cors) — CWE-352
Sec-Fetch-Mode: no-cors)Серьёзность: Средняя
CWE: CWE-352 — Межсайтовая подделка запроса (XSRF/CSRF)
Затрагивает: 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
Защита XSRF в JupyterHub (переработанная в 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. Измените TARGET-JUPYTERHUB в poc/csrf_spawn.html
2. Разместите файл на источнике атакующего (любой статический хост)
3. Откройте его в браузере, уже аутентифицированном на целевом Hub
4. Наблюдайте запуск сервера жертвы без указания токена XSRF
Раскрыто ответственно. PoC опубликован после выпуска исправления.