Jul 23, 2026·5 min read·9 visits
A case-insensitive and syntax-insensitive package name canonicalization issue, combined with a synchronous execution flaw of an asynchronous verification function, allows authenticated users to install blocklisted JupyterLab extensions from PyPI.
GHSA-89VP-JRXV-24W8 is a security bypass vulnerability in the JupyterLab Extension Manager. Authenticated users can install unauthorized or blocklisted extension packages from PyPI. This bypass occurs due to improper canonicalization of package names and the incorrect synchronous invocation of an asynchronous permission-checking method.
JupyterLab is a widely used web-based interactive development environment for Jupyter notebooks, code, and data. The application includes an Extension Manager that allows users to install extra capabilities directly from PyPI. To maintain security in multi-user environments, system administrators can configure extension blocklists or allowlists using configuration variables such as blocked_extensions_uris or allowed_extensions_uris.\n\nHowever, a severe logical flaw in the default PyPI extension manager permitted authenticated users to completely bypass these restrictions. The system relied on string matching of uncanonicalized package names, whereas the underlying installer tool (pip) uses standard normalization policies. Consequently, modifying the casing or inserting standard separator characters allowed restricted packages to evade blocklist policies and install successfully.
The policy enforcement bypass is caused by two distinct logical defects in the backend routing of JupyterLab.\n\nFirst, the validation routine _is_allowed_by_listing in ExtensionManager uses a weak string normalization mechanism. The base class method _normalize_name is implemented as a simple no-op returning the string unaltered. Because the PyPI-specific implementation PyPIExtensionManager does not override this method, the system checks the exact string input by the user against the blocklist.\n\nIn contrast, Python's PEP 503 specification dictates that package names must be normalized by replacing any run of -, _, or . characters with a single - and converting the string to lowercase. Therefore, JupyterLab_Git and jupyterlab-git represent the exact same package to pip but are treated as distinct entities by the JupyterLab validation routine.\n\nSecond, the install function in PyPIExtensionManager calls the asynchronous permission check is_install_allowed synchronously without utilizing the await keyword. In Python, calling an async def function synchronously does not execute its body; instead, it immediately returns a coroutine object. Since all Python coroutine objects evaluate to True in boolean contexts, the condition if not self.is_install_allowed(...) consistently evaluates to False, allowing all installation requests to proceed unconditionally.
Below is an analysis of the vulnerable and patched code patterns in the PyPIExtensionManager and ExtensionManager classes.\n\npython\n# Vulnerable implementation in jupyterlab/extensions/pypi.py\nasync def install(self, name: str, version: Optional[str] = None) -> ActionResult:\n # ...\n # VULNERABILITY: is_install_allowed is async but called synchronously\n if not self.is_install_allowed(name, version):\n return ActionResult(status=\"error\", message=\"install is not allowed\")\n # ...\n\n\nThe corresponding fix introduces the correct asynchronous awaiting mechanism and adds standard PEP 503 canonicalization via packaging.utils.canonicalize_name:\n\npython\n# Patched implementation in jupyterlab/extensions/pypi.py\nfrom packaging.utils import canonicalize_name\n\n@override\ndef _canonicalize_name(self, name: str) -> str:\n \"\"\"Canonicalize PyPI package names for listing policy comparisons.\"\"\"\n return canonicalize_name(name)\n\nasync def install(self, name: str, version: Optional[str] = None) -> ActionResult:\n # ...\n # FIX: Correctly awaiting the asynchronous permission check\n if not await self.is_install_allowed(name, version):\n return ActionResult(status=\"error\", message=\"install is not allowed\")\n # ...\n\n\nmermaid\ngraph LR\n A[\"User requests package: 'JupyterLab_Git'\"] --> B{\"is_install_allowed()\"}\n B -->|Return Coroutine| C{\"Coroutine evaluates to True\"}\n C -->|Check Bypassed| D[\"pip install JupyterLab_Git\"]\n D --> E[\"Package 'jupyterlab-git' is installed\"]\n
To exploit this vulnerability, an attacker must have authenticated access to the JupyterLab environment and the default PyPI extension manager must be enabled.\n\nAn attacker initiates the installation of a restricted extension by issuing a POST request to the JupyterLab API. If the administrator blocklisted a package named jupyterlab-malicious-ext, the attacker supplies a permutation such as JupyterLab.Malicious.Ext in the installation command.\n\nDue to the synchronous execution of the asynchronous check, the server ignores the restriction. Even if that check were bypassed, the string verification fails to match JupyterLab.Malicious.Ext with jupyterlab-malicious-ext. The server subsequently spawns a backend subprocess to execute pip install JupyterLab.Malicious.Ext. The PEP 503-compliant package manager resolves the name to the forbidden package and installs it, executing any malicious installer hooks or payload scripts included in the package package setup.
The direct impact of this vulnerability is the complete circumvention of administrator-defined package restrictions in JupyterLab. This allows any authenticated user with access to the Extension Manager to run arbitrary Python code under the privileges of the JupyterLab server process.\n\nIn shared hosting, JupyterHub deployments, or enterprise notebook environments, this compromise permits horizontal privilege escalation, data exfiltration from user spaces, and potential host compromise if the server is not isolated properly inside a container. The CVSS score of 6.1 represents Medium severity because exploitation requires authentication, but the technical outcome results in high integrity loss.
The primary remediation for this vulnerability is upgrading the jupyterlab package to one of the officially patched versions.\n\nDeployments on the 4.5.x release branch must be upgraded to version 4.5.10 or higher. Deployments on the 4.6.x branch must be upgraded to 4.6.2 or higher. The package upgrades can be accomplished via pip or conda commands.\n\nIf immediate patching is unfeasible, administrators can mitigate the vulnerability by restricting extension installation entirely. This is achieved by running the Jupyter Server with the CLI flag --LabApp.extension_manager=readonly or by specifying the setting c.LabApp.extension_manager = 'readonly' inside the jupyter_lab_config.py configuration file.
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
jupyterlab Project Jupyter | >= 4.5.0, <= 4.5.9 | 4.5.10 |
jupyterlab Project Jupyter | >= 4.6.0, <= 4.6.1 | 4.6.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-180 |
| Attack Vector | Network |
| CVSS Score | 6.1 |
| Exploit Status | poc |
| CISA KEV Status | false |
The product validates input before canonicalizing it, which can allow an attacker to bypass access controls.
An improper access control vulnerability in JupyterLab allows programmatic installation of blocked or non-allowed extensions. The vulnerability is caused by a missing await keyword when invoking the asynchronous checking method, leading Python to evaluate the returned coroutine object as truthy. Furthermore, the check lacks proper canonicalization of package names, enabling an attacker to bypass blocklists using casing or character variants.
An unsigned 32-bit integer underflow vulnerability in the Windows Management Instrumentation (WMI) serialization subsystem of ntoskrnl.exe allows local authenticated users with low privileges to corrupt adjacent kernel pool allocations, execute arbitrary kernel-mode read and write operations, and perform a Token Swap attack to escalate their privileges to NT AUTHORITY\SYSTEM.
JupyterLab's PluginManager contains an authorization bypass vulnerability allowing authenticated users to modify the state of locked extensions or plugins. Although the frontend user interface visually locks and disables toggle components for administrative configurations, the backend API fails to perform robust validation. Standard users can craft direct HTTP API requests to modify plugin states, completely bypassing administrative restrictions.
A stored Cross-Site Scripting (XSS) vulnerability exists in JupyterLab's Image Viewer component when processing Scalable Vector Graphics (SVG) images. Due to the lingering lifecycle of generated object URLs and the inheritance of the application origin by client-side Blobs, an attacker can execute arbitrary JavaScript within the victim's active session. This execution occurs when a user views an SVG file in the JupyterLab image viewer, right-clicks the image, and selects 'Open image in new tab'.
JupyterLab versions 4.5.x and 4.6.x prior to 4.5.10 and 4.6.2 are vulnerable to a DOM-based Cross-Site Scripting (XSS) vulnerability. An attacker can craft a malicious overrides.json file that executes arbitrary JavaScript inside the victim's browser session. This can occur either automatically if the file is pre-planted on a multi-tenant filesystem, or via user interaction when importing settings.
A module-cache poisoning vulnerability exists in the n8n JavaScript task runner. In multi-tenant or multi-user n8n deployments, custom code executed inside Code nodes shares a single Node.js process and memory space. If custom module loading is enabled via configuration, an authorized user can dynamically patch (monkey-patch) globally cached modules. Subsequent executions of workflows by other tenants or users that load the same module will receive the poisoned reference, resulting in cross-tenant data exposure, credential hijacking, or integrity compromise.