
Prova de conceito para CVE-2026-40864: bypass de XSRF no JupyterHub por meio de POST de formulário cross-origin explorando Sec-Fetch-Mode: no-cors. Inclui HTML de PoC, análise da causa raiz e cronologia da divulgação.
Sec-Fetch-Mode: no-cors)Severidade: Média (CVSS 5.4)
CWE: CWE-352 — Cross-Site Request Forgery (XSRF)
Afetado: jupyterhub 4.1.0 ≤ versão < 5.4.5 (corrigido em 5.4.5)
Aviso: GHSA-m68r-v472-jgq9
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40864
Crédito: Romain Deperne
A proteção XSRF do JupyterHub (reformulada na 4.1.0) usava o cabeçalho de requisição Sec-Fetch-Mode para decidir
se uma requisição era same-origin. Ele tratava Sec-Fetch-Mode: no-cors como same-origin — mas
no-cors é exatamente o que um navegador envia para uma submissão de formulário "simples" cross-origin. Como
resultado, POSTs de formulários HTML cross-origin para os endpoints de formulário do Hub (/hub/spawn, /hub/accept-share)
contornavam completamente a verificação XSRF.
A API JSON não é afetada (ela exige um content type não-simples, o que força um preflight CORS). Apenas os endpoints de formulário HTML são alcançáveis dessa forma.
A lógica XSRF faz um curto-circuito para "confiável" para um conjunto de estados Sec-Fetch-* destinados a capturar
navegações same-origin. Sec-Fetch-Mode: no-cors estava incluído nesse conjunto confiável. Mas no-cors
é o modo que o navegador atribui a um <form method=POST> simples enviado para uma origem diferente —
precisamente o vetor CSRF clássico que o token deveria impedir. Portanto, qualquer endpoint que altera estado que
aceite um corpo de formulário simples, e dependa exclusivamente desse portão XSRF, é forjável cross-origin.
Mapeando isso para endpoints reais: /hub/spawn (iniciar o servidor da vítima) e /hub/accept-share
(fazer a vítima aceitar um compartilhamento do servidor do atacante) são ambos POSTs de formulário atrás desse portão.
/hub/spawn — uma página do atacante pode iniciar o servidor single-user da vítima sem consentimento
(consumo de recursos / estado inesperado; o atacante não obtém acesso a esse servidor)./hub/accept-share — quando o atacante é um usuário do JupyterHub autorizado a compartilhar seu próprio
servidor, ele pode forçar uma vítima a aceitar um compartilhamento, dando à vítima acesso ao servidor do
atacante (uma etapa de preparação para cenários posteriores de engenharia social / entrega de dados).Usar Sec-Fetch-Mode como oráculo de origem é falho: no-cors não implica same-origin. A correção
na 5.4.5 deixa de confiar em no-cors como same-origin. Operadores que não podem atualizar imediatamente podem descartar
requisições que carreguem Sec-Fetch-Mode: no-cors no proxy reverso.
poc/csrf_spawn.html — hospede-o em qualquer origem do atacante e faça um usuário autenticado do JupyterHub abri-lo.
O formulário de envio automático emite um POST cross-origin para /hub/spawn (o navegador envia
Sec-Fetch-Mode: no-cors); builds vulneráveis o aceitam sem nenhum token _xsrf válido e iniciam o
servidor da vítima. Aponte action para /hub/accept-share para a variante de aceitação de compartilhamento.
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 responsável. PoC publicado após a correção ser lançada.