Jupyter server on Windows discloses Windows user password hash
Description
The Jupyter Server provides the backend for Jupyter web applications. Jupyter Server on Windows has a vulnerability that lets unauthenticated attackers leak the NTLMv2 password hash of the Windows user running the Jupyter server. An attacker can crack this password to gain access to the Windows machine hosting the Jupyter server, or access other network-accessible machines or 3rd party services using that credential. Or an attacker perform an NTLM relay attack without cracking the credential to gain access to other network-accessible machines. This vulnerability is fixed in 2.14.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jupyter Server on Windows leaks the NTLMv2 password hash of the authenticated Windows user to unauthenticated attackers, allowing credential theft or relay attacks.
Jupyter Server, the backend for Jupyter web applications, contains a vulnerability that exposes the NTLMv2 password hash of the Windows user running the server. The issue occurs only on Windows systems and allows unauthenticated remote attackers to leak this sensitive credential hash. The root cause involves improper handling of file path requests, which can trigger an NTLM authentication attempt that leaks the hash.
An attacker can exploit this vulnerability by sending a specially crafted request to the Jupyter Server without needing any authentication. The attack does not require user interaction and can be performed over the network if the Jupyter Server is accessible. The leaked NTLMv2 hash can then be cracked offline to recover the plaintext password, or used directly in an NTLM relay attack to impersonate the victim on other network-accessible machines.
The impact is severe: an attacker who obtains the hash can gain full access to the Windows host running Jupyter Server, as well as any other network resources or third-party services that trust the compromised Windows credentials. This could lead to lateral movement within an organization's network and potential data breaches. The vulnerability is rated with a CVSS score of 8.8 (High) according to the NVD.
Jupyter Server version 2.14.1 fixes this vulnerability by modifying the file path handling to prevent the unintended NTLM authentication leak. Administrators running Jupyter Server on Windows should update to this patched version immediately. There are no known workarounds aside from upgrading [1][2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
jupyter_serverPyPI | < 2.14.1 | 2.14.1 |
Affected products
24- osv-coords23 versionspkg:apk/chainguard/kubeflow-pipelines-visualization-serverpkg:apk/chainguard/py3.10-jupyter-serverpkg:apk/chainguard/py3.10-jupyter-server-binpkg:apk/chainguard/py3.11-jupyter-serverpkg:apk/chainguard/py3.11-jupyter-server-binpkg:apk/chainguard/py3.12-jupyter-serverpkg:apk/chainguard/py3.12-jupyter-server-binpkg:apk/chainguard/py3.13-jupyter-serverpkg:apk/chainguard/py3.13-jupyter-server-binpkg:apk/chainguard/py3-jupyter-serverpkg:apk/chainguard/py3-supported-jupyter-serverpkg:apk/wolfi/kubeflow-pipelines-visualization-serverpkg:apk/wolfi/py3.10-jupyter-serverpkg:apk/wolfi/py3.10-jupyter-server-binpkg:apk/wolfi/py3.11-jupyter-serverpkg:apk/wolfi/py3.11-jupyter-server-binpkg:apk/wolfi/py3.12-jupyter-serverpkg:apk/wolfi/py3.12-jupyter-server-binpkg:apk/wolfi/py3.13-jupyter-serverpkg:apk/wolfi/py3.13-jupyter-server-binpkg:apk/wolfi/py3-jupyter-serverpkg:apk/wolfi/py3-supported-jupyter-serverpkg:pypi/jupyter_server
< 2.4.0-r0+ 22 more
- (no CPE)range: < 2.4.0-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.4.0-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1-r0
- (no CPE)range: < 2.14.1
- Range: < 2.14.1
Patches
179fbf801c590Merge pull request from GHSA-hrw6-wg82-cm62
2 files changed · +81 −57
jupyter_server/utils.py+42 −57 modified@@ -11,6 +11,7 @@ import sys import warnings from contextlib import contextmanager +from pathlib import Path from typing import Any, Generator, NewType, Sequence from urllib.parse import ( SplitResult, @@ -338,81 +339,65 @@ def is_namespace_package(namespace: str) -> bool | None: return isinstance(spec.submodule_search_locations, _NamespacePath) -def filefind(filename: str, path_dirs: Sequence[str] | str | None = None) -> str: +def filefind(filename: str, path_dirs: Sequence[str]) -> str: """Find a file by looking through a sequence of paths. - This iterates through a sequence of paths looking for a file and returns - the full, absolute path of the first occurrence of the file. If no set of - path dirs is given, the filename is tested as is, after running through - :func:`expandvars` and :func:`expanduser`. Thus a simple call:: - filefind("myfile.txt") + For use in FileFindHandler. - will find the file in the current working dir, but:: + Iterates through a sequence of paths looking for a file and returns + the full, absolute path of the first occurrence of the file. - filefind("~/myfile.txt") + Absolute paths are not accepted for inputs. - Will find the file in the users home directory. This function does not - automatically try any paths, such as the cwd or the user's home directory. + This function does not automatically try any paths, + such as the cwd or the user's home directory. Parameters ---------- filename : str - The filename to look for. - path_dirs : str, None or sequence of str - The sequence of paths to look for the file in. If None, the filename - need to be absolute or be in the cwd. If a string, the string is - put into a sequence and the searched. If a sequence, walk through - each element and join with ``filename``, calling :func:`expandvars` - and :func:`expanduser` before testing for existence. + The filename to look for. Must be a relative path. + path_dirs : sequence of str + The sequence of paths to look in for the file. + Walk through each element and join with ``filename``. + Only after ensuring the path resolves within the directory is it checked for existence. Returns ------- - Raises :exc:`IOError` or returns absolute path to file. + Raises :exc:`OSError` or returns absolute path to file. """ - - # If paths are quoted, abspath gets confused, strip them... - filename = filename.strip('"').strip("'") - # If the input is an absolute path, just check it exists - if os.path.isabs(filename) and os.path.isfile(filename): - return filename - - if path_dirs is None: - path_dirs = ("",) - elif isinstance(path_dirs, str): - path_dirs = (path_dirs,) - - for path in path_dirs: - if path == ".": - path = os.getcwd() # noqa: PLW2901 - testname = expand_path(os.path.join(path, filename)) - if os.path.isfile(testname): - return os.path.abspath(testname) + file_path = Path(filename) + + # If the input is an absolute path, reject it + if file_path.is_absolute(): + msg = f"{filename} is absolute, filefind only accepts relative paths." + raise OSError(msg) + + for path_str in path_dirs: + path = Path(path_str).absolute() + test_path = path / file_path + # os.path.abspath resolves '..', but Path.absolute() doesn't + # Path.resolve() does, but traverses symlinks, which we don't want + test_path = Path(os.path.abspath(test_path)) + if sys.version_info >= (3, 9): + if not test_path.is_relative_to(path): + # points outside root, e.g. via `filename='../foo'` + continue + else: + # is_relative_to is new in 3.9 + try: + test_path.relative_to(path) + except ValueError: + # points outside root, e.g. via `filename='../foo'` + continue + # make sure we don't call is_file before we know it's a file within a prefix + # GHSA-hrw6-wg82-cm62 - can leak password hash on windows. + if test_path.is_file(): + return os.path.abspath(test_path) msg = f"File {filename!r} does not exist in any of the search paths: {path_dirs!r}" raise OSError(msg) -def expand_path(s: str) -> str: - """Expand $VARS and ~names in a string, like a shell - - :Examples: - In [2]: os.environ['FOO']='test' - In [3]: expand_path('variable FOO is $FOO') - Out[3]: 'variable FOO is test' - """ - # This is a pretty subtle hack. When expand user is given a UNC path - # on Windows (\\server\share$\%username%), os.path.expandvars, removes - # the $ to get (\\server\share\%username%). I think it considered $ - # alone an empty var. But, we need the $ to remains there (it indicates - # a hidden share). - if os.name == "nt": - s = s.replace("$\\", "IPYTHON_TEMP") - s = os.path.expandvars(os.path.expanduser(s)) - if os.name == "nt": - s = s.replace("IPYTHON_TEMP", "$\\") - return s - - def import_item(name: str) -> Any: """Import and return ``bar`` given the string ``foo.bar``. Calling ``bar = import_item("foo.bar")`` is the functional equivalent of
tests/test_utils.py+39 −0 modified@@ -13,6 +13,7 @@ from jupyter_server.utils import ( check_pid, check_version, + filefind, is_namespace_package, path2url, run_sync_in_loop, @@ -125,3 +126,41 @@ def test_unix_socket_in_use(tmp_path): sock.listen(0) assert unix_socket_in_use(server_address) sock.close() + + +@pytest.mark.parametrize( + "filename, result", + [ + ("/foo", OSError), + ("../c/in-c", OSError), + ("in-a", "a/in-a"), + ("in-b", "b/in-b"), + ("in-both", "a/in-both"), + (r"\in-a", OSError), + ("not-found", OSError), + ], +) +def test_filefind(tmp_path, filename, result): + a = tmp_path / "a" + a.mkdir() + b = tmp_path / "b" + b.mkdir() + c = tmp_path / "c" + c.mkdir() + for parent in (a, b): + with parent.joinpath("in-both").open("w"): + pass + with a.joinpath("in-a").open("w"): + pass + with b.joinpath("in-b").open("w"): + pass + with c.joinpath("in-c").open("w"): + pass + + if isinstance(result, str): + found = filefind(filename, [str(a), str(b)]) + found_relative = Path(found).relative_to(tmp_path) + assert str(found_relative) == result + else: + with pytest.raises(result): + filefind(filename, [str(a), str(b)])
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-hrw6-wg82-cm62ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-35178ghsaADVISORY
- github.com/jupyter-server/jupyter_server/commit/79fbf801c5908f4d1d9bc90004b74cfaaeeed2dfghsax_refsource_MISCWEB
- github.com/jupyter-server/jupyter_server/security/advisories/GHSA-hrw6-wg82-cm62ghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/jupyter-server/PYSEC-2024-165.yamlghsaWEB
News mentions
0No linked articles in our index yet.