JupyterHub Cross-Origin Form POST XSRF Bypass (CVE-2026-40864)
by Romain Deperne · 2026-07-05
- Severity
- Medium
- CVE
- CVE-2026-40864 (GHSA-m68r-v472-jgq9)
- Category
- web
- Affected product
- JupyterHub
- Affected versions
- 4.1.0 <= version < 5.4.5 (fixed in 5.4.5)
- Disclosed
- 2026-07-05
- Patch status
- patched
Archive entry
intelseclab/poc-archiveMetadata
| Field | Value |
|---|---|
| Date Added | 2026-07-05 |
| Last Updated | 2026-06 |
| Author / Researcher | Romain Deperne |
| CVE / Advisory | CVE-2026-40864 (GHSA-m68r-v472-jgq9) |
| Category | web |
| Severity | Medium |
| CVSS Score | Not specified in source |
| Status | PoC |
| Tags | jupyterhub, csrf, xsrf-bypass, sec-fetch-mode, cwe-352, python, jupyter, session-hijack |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | JupyterHub |
| Versions Affected | 4.1.0 <= version < 5.4.5 (fixed in 5.4.5) |
| Language / Platform | HTML/JavaScript PoC against a Python (Tornado) web application |
| Authentication Required | Yes (victim must be an authenticated JupyterHub user) |
| Network Access Required | Yes |
Summary
JupyterHub’s XSRF protection, reworked in 4.1.0, uses the browser-supplied Sec-Fetch-Mode header as an origin oracle to decide whether a request is same-origin and therefore exempt from token validation. The implementation incorrectly treats Sec-Fetch-Mode: no-cors as a same-origin signal, but browsers send exactly that mode for a plain cross-origin <form method="POST"> submission — the textbook CSRF vector the token exists to stop. As a result, the Hub’s HTML form endpoints (/hub/spawn and /hub/accept-share) can be forged from any external origin with no valid _xsrf token. The JSON API is unaffected because it requires a non-simple content type that forces a CORS preflight. The included PoC is a self-submitting HTML form that demonstrates the bypass against /hub/spawn.
Vulnerability Details
Root Cause
The XSRF check short-circuits to “trusted” whenever Sec-Fetch-Mode is no-cors, but no-cors does not imply same-origin — it is precisely the mode assigned to a simple cross-origin form POST, so the same-origin assumption underlying the check is unsound.
Attack Vector
- Attacker hosts an auto-submitting HTML page containing a hidden
<form>targeting the victim’s JupyterHub instance (/hub/spawnor/hub/accept-share). - Attacker lures a logged-in JupyterHub user into visiting the page.
- The browser issues the cross-origin POST with
Sec-Fetch-Mode: no-cors; the victim’s session cookie is sent automatically. - Vulnerable JupyterHub accepts the request without a valid
_xsrftoken, executing the action as the victim.
Impact
An attacker can force a victim to spawn their own single-user server without consent (resource abuse / unexpected state), or — for /hub/accept-share — force the victim to accept a share of the attacker’s server, which can be used to stage further social-engineering or data-exfiltration scenarios.
Environment / Lab Setup
Target: JupyterHub 4.1.0 – 5.4.4 (self-hosted or containerized instance)
Attacker: Any static file host or local HTTP server to serve csrf_spawn.html; a browser session already authenticated to the target Hub
Proof of Concept
PoC Script
See
csrf_spawn.htmlin this folder.
| |
The page auto-submits a hidden form (with an empty _xsrf field) to the target Hub’s /hub/spawn endpoint the moment it loads. Because the browser sends Sec-Fetch-Mode: no-cors for this simple form POST, vulnerable JupyterHub builds treat it as same-origin and process the spawn request without a valid token. Editing the action attribute to /hub/accept-share demonstrates the share-acceptance variant.
Detection & Indicators of Compromise
Signs of compromise:
- Server spawn events in JupyterHub audit logs with no corresponding user-initiated UI action
- Unexpected
accept-shareevents granting access to servers the victim did not intend to use - Requests to
/hub/spawnor/hub/accept-sharecarryingSec-Fetch-Mode: no-corsand a foreignOrigin/Referer
Remediation
| Action | Detail |
|---|---|
| Primary fix | Upgrade to JupyterHub 5.4.5 or later, which no longer trusts no-cors as a same-origin indicator |
| Interim mitigation | At the reverse proxy in front of JupyterHub, drop requests carrying Sec-Fetch-Mode: no-cors to the Hub’s form endpoints |
References
Notes
Mirrored from https://github.com/romain-deperne/CVE-2026-40864 on 2026-07-05.
| |