
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)Gravidade: Moderada
CWE: CWE-352 — Cross-Site Request Forgery (XSRF)
Afetado: jupyterhub 4.1.0 ≤ versão < 5.4.5 (corrigido na 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 versão 4.1.0) usava o cabeçalho de requisição Sec-Fetch-Mode para decidir se uma requisição era de mesma origem. Ela tratava Sec-Fetch-Mode: no-cors como mesma origem — mas no-cors é exatamente o que um navegador envia para uma . Como resultado, POSTs de formulários HTML entre origens para os endpoints de formulário do Hub (, ) ignoravam completamente a verificação XSRF.
/hub/spawn/hub/accept-shareA API JSON não é afetada (ela exige um tipo de conteúdo não simples, o que força um preflight CORS). Somente os endpoints de formulário HTML são alcançáveis dessa forma.
A lógica XSRF retorna imediatamente "confiável" para um conjunto de estados Sec-Fetch-* criados para capturar navegações de mesma origem. Sec-Fetch-Mode: no-cors estava incluído nesse conjunto confiável. Porém no-cors é o modo que o navegador atribui a um <form method=POST> simples que envia para uma origem diferente — exatamente o vetor clássico de CSRF que o token deveria impedir. Assim, qualquer endpoint que altera estado, aceita um corpo de formulário simples e depende apenas dessa barreira XSRF pode ser forjado entre origens.
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 protegidos por essa barreira.
/hub/spawn — uma página do atacante pode iniciar o servidor de usuário único 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 com permissão para compartilhar o próprio servidor, ele pode forçar uma vítima a aceitar um compartilhamento, dando à vítima acesso ao servidor do atacante (uma etapa preparatória para cenários adicionais de engenharia social / depósito de dados).Usar Sec-Fetch-Mode como um oráculo de origem não é sólido: no-cors não implica mesma origem. A correção na versão 5.4.5 deixa de confiar em no-cors como mesma origem. Operadores que não puderem atualizar imediatamente podem descartar, no proxy reverso, requisições que carreguem Sec-Fetch-Mode: no-cors.
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 entre origens para /hub/spawn (o navegador envia Sec-Fetch-Mode: no-cors); versões vulneráveis aceitam o POST sem um 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 depois que a correção foi lançada.