Jul 23, 2026·5 min read·4 visits
An authorization validation bypass and configuration mapping bug in JupyterLab's PluginManager allow authenticated users to circumvent extension locks and disable or enable arbitrary plugins through direct API requests.
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.
JupyterLab relies on an extensible plugin system where individual components control standard interface capabilities, administrative rules, and external network interactions.
Administrators in shared deployments, such as JupyterHub environments, often use lock policies to prevent standard users from disabling critical controls. These locked components may include auditing scripts, storage restrictions, download limits, or proxy features.
Although the frontend application visually disables the relevant interface buttons, the backend API endpoint (/lab/api/plugins) fails to robustly enforce authorization limits when accessed directly. Standard users can exploit this server-side gap to execute commands on locked extensions, violating administrative boundaries.
The root cause of this vulnerability involves two distinct server-side validation gaps in the JupyterLab backend infrastructure.
The first gap lies in how lock rules are evaluated inside jupyterlab/extensions/manager.py. In JupyterLab, plugin identifiers follow a hierarchical format consisting of the parent extension name and the child plugin name separated by a colon, such as <extension-name>:<plugin-identifier>.
When administrators apply a lock to an entire extension, only the parent extension name is stored in the lock registry. When a standard user sends a request to disable a specific plugin, the _find_locked method only checks if the exact, fully qualified child identifier is in the lock registry. Because it fails to parse and inspect the parent portion of the identifier, the server incorrectly determines that the plugin is unlocked.
The second gap stems from a configuration mismatch during the application startup configuration. Standard server initialization permits the option --LabApp.lock_all_plugins=True to globally lock all plugins. The launcher in jupyterlab/labapp.py passes this configuration parameter using the key "all_locked", but the PluginManager initialization logic expects the key "lock_all". This typographical error causes the global lock setting to be silently dropped during server initialization.
Examining the patch diff reveals the corrective validation and key-mapping changes across the affected files.
In jupyterlab/extensions/manager.py, the validation check was refactored to parse child plugin strings and check both the specific identifier and the parent extension identifier against the lock rules list.
# Vulnerable Implementation
def _find_locked(self, plugins_or_extensions: list[str]) -> frozenset[str]:
for plugin in plugins_or_extensions:
if ":" in plugin:
# Only verifies the exact child identifier string
if plugin in self.options.lock_rules:
locked_subset.add(plugin)
# Patched Implementation
def _find_locked(self, plugins_or_extensions: list[str]) -> frozenset[str]:
for plugin in plugins_or_extensions:
if ":" in plugin:
# Extracts parent extension name and checks both levels
extension = plugin.split(":")[0]
if plugin in self.options.lock_rules or extension in self.options.lock_rules:
locked_subset.add(plugin)Additionally, the option dictionary key in jupyterlab/labapp.py was aligned with the exact string that the backend validation engine expected to find.
# Vulnerable Mapping
ext_options={
"lock_rules": lock_rules,
"all_locked": self.lock_all_plugins,
}
# Patched Mapping
ext_options={
"lock_rules": lock_rules,
"lock_all": self.lock_all_plugins,
}These modifications ensure that both targeted parent locks and global lock policies are correctly parsed, loaded, and evaluated by the server backend.
To exploit this vulnerability, an attacker must have authenticated access to a standard user account on the target JupyterLab server. Because the frontend interface prevents toggling via standard menus, the attacker bypasses the graphical console completely.
The attacker retrieves their active session cookie or authorization header and structures a manual HTTP POST request directed at /lab/api/plugins. This payload contains the parameters to disable the target security or auditing plugin.
POST /lab/api/plugins HTTP/1.1
Host: target-jupyterlab-server.com
Content-Type: application/json
Authorization: Token [user-api-token]
{
"cmd": "disable",
"plugin_name": "jupyterlab-hardening-extension:download-limiter"
}Because the administrative lock was applied to the parent extension (jupyterlab-hardening-extension) and not the explicit child string, the backend verification function determines that the plugin is unlocked. The server executes the modification, disabling the designated control and returning a success status code to the client.
This vulnerability is highly significant in multi-user Jupyter environments such as JupyterHub configurations, where host controls depend on locked plugins.
By systematically disabling locked plugins, an attacker can bypass operational limitations, escape container restrictions, and avoid active monitoring controls. This effectively deactivates any local auditing tools or transfer limitations applied by administrative security frameworks.
However, this vulnerability does not directly permit initial remote code execution or privilege escalation on the underlying host operating system. The attacker must possess authenticated login credentials to interact with the API, making it an internal post-authentication policy evasion vector.
The standard and recommended remediation path is to upgrade JupyterLab to the officially patched versions.
Administrators using the 4.5.x release line should update to version 4.5.10 or higher, and those on the 4.6.x line should upgrade to version 4.6.2 or higher. Any downstream wrapper distributions should have their requirements updated to reflect these dependency parameters.
If patching is not immediately feasible, administrators can temporarily mitigate the vulnerability by changing their deployment policies. Rather than applying locks at the parent extension level, lock files must register each child plugin identifier individually.
jupyter labextension lock <extension-name>:<child-plugin-id-1>
jupyter labextension lock <extension-name>:<child-plugin-id-2>This explicit locking ensures that direct API queries will match the exact strings in the lock list, neutralizing the parent parsing bypass.
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
jupyterlab Project Jupyter | >= 4.1.0, <= 4.5.9 | 4.5.10 |
jupyterlab Project Jupyter | >= 4.6.0, <= 4.6.1 | 4.6.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-602, CWE-863 |
| Attack Vector | Network |
| CVSS | 6.0 |
| Impact | High Integrity, Low Confidentiality |
| Exploit Status | PoC Available |
| KEV Status | Not Listed |
The application's UI visually disables/locks the interface components to prevent users from toggling extension states, but the backend fails to properly validate and reject direct API state-change requests from the client.
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.
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.
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.