{
  "ticket_id": "CVE-2026-35397",
  "code_root": "external/jupyter_server",
  "source": {
    "type": "cve",
    "cve_id": "CVE-2026-35397",
    "advisory_id": "GHSA-5789-5fc7-67v3",
    "advisory_url": "https://github.com/jupyter-server/jupyter_server/security/advisories/GHSA-5789-5fc7-67v3",
    "vendor": "jupyter-server",
    "product": "jupyter-server",
    "repo": "https://github.com/jupyter-server/jupyter_server"
  },
  "facts": {
    "cve_id": "CVE-2026-35397",
    "advisory_id": "GHSA-5789-5fc7-67v3",
    "issue_summary": "Jupyter Server enforces that requested files stay inside the configured root directory by comparing the resolved requested path against the configured root with a plain string startswith() check. Because startswith() tests string prefixes and not path-component boundaries, a sibling directory whose name merely shares a prefix with the root (root '/srv/data', sibling '/srv/data-secret') satisfies the check. An attacker can therefore request files located outside the configured root directory, in any directory whose path begins with the root path string.",
    "vulnerability_type": "Path Traversal (faulty startswith() containment check)",
    "suspected_cwe": ["CWE-22"],
    "affected_versions": "<= 2.17.0",
    "fixed_versions": ["2.18.0"],
    "reproduce_version": "2.17.0",
    "verify_fixed_version": "2.18.0",
    "repo_url": "https://github.com/jupyter-server/jupyter_server.git",
    "code_root": "external/jupyter_server",
    "ecosystem": "PyPI",
    "package": "jupyter-server",
    "install_recipe": "In a scratch directory run `pip install jupyter-server==2.17.0` (vulnerable) or `pip install jupyter-server==2.18.0` (fixed). The published PyPI wheels are the canonical artifacts; git tags v2.17.0 / v2.18.0 on the repo are equivalent.",
    "attacker_access": "Any client that can reach the Jupyter Server HTTP endpoints and request a file path.",
    "exploit_outline": "Configure the server with a root directory (e.g. `/srv/data`). Create a sibling directory whose name shares the root's prefix (e.g. `/srv/data-secret`) and place a file inside it. Request that sibling file via the server's file/contents endpoint. The startswith() containment check treats `/srv/data-secret/...` as being inside `/srv/data` because the string starts with the root.",
    "vulnerable_indicator": "On 2.17.0 the request for the prefix-sharing sibling file succeeds: HTTP 200 with the file contents in the response body.",
    "fixed_indicator": "On 2.18.0 the containment check uses a proper path-boundary comparison; the request for the sibling file is rejected with HTTP 403 or 404 and the file contents are not returned.",
    "reproduction_requirements": "Python plus the jupyter-server package at the specified version, started with an explicit root directory. A prefix-sharing sibling directory and a target file on the local filesystem. No database or browser required - the difference is observable in the HTTP response.",
    "notes": "The root cause is the use of string prefix matching (startswith()) instead of a path-component-aware containment check; the fix in 2.18.0 corrects the comparison so prefix-sharing siblings are no longer treated as inside the root."
  }
}
