In September of 2024, ZDI received a vulnerability submission from an anonymous researcher affecting npm CLI that revealed a fundamental design issue in Node.js. This blog details how it continues to expose applications to local privilege escalation (LPE) attacks on Windows systems, including the Discord desktop app (CVE-2026-0776 0-Day), which remains unpatched and vulnerable.
The issue is straightforward: when Node.js resolves modules, the runtime searches for packages in C:\node_modules as part of its default behavior. Since low-privileged Windows users can create this directory and plant malicious modules there, any Node.js application with missing or optional dependencies becomes vulnerable to privilege escalation.
This issue is not new. Concerned discussions about Node.js's module search path behavior date back to 2013 and 2014.
Node.js has explicitly stated that they consider this behavior intentional:
"Node.js trusts the file system."
They do not treat CWE-427 (Uncontrolled Search Path Element) as a vulnerability, pushing responsibility onto application developers.
Figure 1: The vendor’s security policy stance on CWE-427 as a non-issue
As the case studies below demonstrate, this stance has dangerous consequences. Developers are largely unaware of this attack surface, and the result is a proliferation of exploitable applications. We will show examples in npm CLI and Discord, but there are likely many more applications that are impacted by this.
Root Cause
The root cause lies in the way Node.js performs module resolution. This is documented here. Although UNIX paths are used in the documentation provided by Node.js, the same logic is applied on Windows.
When a Node.js application calls require(‘bar’), the runtime searches for the module in the following order:
- C:\Users\Administrator\projects\node_modules\bar.js
- C:\Users\Administrator\node_modules\bar.js
- C:\Users\node_modules\bar.js
- C:\node_modules\bar.js <-- The problem
If the legitimate package is missing, whether due to optional dependencies, development packages removed in production, or installation failures, the resolution search will eventually reach the root of the drive. Any user can create C:\node_modules and place a malicious package there. Once the low-privileged user has populated C:\node_modules\bar.js, Node.js will load and execute it in the context of the current user. In the following case studies, we will provide evidence of how, despite properly following NPM’s guidelines, third-party dependencies end up triggering this vulnerability anytime you launch the application.
Case Studies: Real-World Manifestations
The Optional Dependency Pattern: npm supports optional dependencies to be specified in the project’s package.json file. The recommended pattern for checking for these dependencies is as follows:
Figure 2: npm Docs showing optionalDependencies example code
This pattern silently catches errors when optional packages are missing, allowing execution to continue. So what’s the problem? On Windows, Node.js will search all the way up to C:\node_modules where an attacker may have planted a malicious replacement. This search behavior mirrors UNIX conventions where /node_modules at the filesystem root is typically only writable by root. Windows systems by default allow any user to create C:\node_modules. Once require is called, Node.js will traverse the search path and execute any matching module it finds.
Important things to note:
- This pattern can be found in third party libraries deep in a dependency tree, as we will see in the following examples.
- There is no runtime indication to either the developers or the end users that such a vulnerability exists without looking at the filesystem logs with Procmon.
- The optional dependency pattern itself would not be dangerous if Node.js did not search for packages in
C:\node_modules.
Let’s take a deeper look at both cases and see why this is so dangerous.
Case 1: npm CLI (ZDI-26-043 / ZDI-CAN-25430 / CVE-2026-0775).
Prior to version 11.2.0, npm CLI used a library called “promise-inflight”, which contained an optional dependency on a package called “bluebird”.
When Node.js is installed on the system, npm is included by default without the bluebird package. This vulnerability was introduced when bluebird was removed through a well-intentioned pull request (https://github.com/npm/cli/pull/1438/changes), demonstrating how easy it is for developers to unknowingly create this attack surface.
We can see Node’s package resolution logic at work in the screenshot below:
Figure 4: Procmon log showing the package resolution behavior of Node.js via CVE-2026-0775
First, the application looks for the bluebird.js package in the Node.js installation directory. Node.js sequentially searches back to the system root until it finds the package. If an attacker has placed C:\node_modules\bluebird.js, the require call will find, read, and execute the malicious payload in the context of any user running npm on the system.
This vulnerability is especially dangerous because it is triggered when many npm * cli commands are used. Common development commands such as npm install, npm –l, and npm prune will all execute the malicious bluebird.jspackage.
Case 2: Discord (ZDI-26-040/ ZDI-CAN-27057 / CVE-2026-0776/ UNPATCHED)
On April 22, 2025, ZDI received a report for a similar vulnerability in Discord reported by T. Doğa Gelişli. Discord uses the ws WebSocket library, which contains an optional dependency on utf-8-validate for compatibility with older Node.js versions:
Figure 5: websockets library repo snippet showing require call for missing utf-8-validate package dependency
Discord does not ship with the utf-8-validate package. As a result, the following Procmon logs show the same behavior as Case 1. Anytime Discord is launched, the attacker controlled C:\node_modules\utf-8-validate.js is executed.
Figure 6: Procmon log showing the package resolution behavior of Node.js via CVE-2026-0776
The ws library does support disabling this check via the WS_NO_UTF_8_VALIDATE environment variable, but this requires the consuming application (Discord) to set it explicitly. Here’s a quick video demonstrating the bug by popping the calc app when opening Discord:
Discord automatically opens on login by default, so in practice code execution happens immediately without any user interaction. Strangely, the Discord Security team made it clear to us in their responses that they do not consider local attack vectors as valid security issues.
The Bigger Picture
The cases above represent only a few of the applications affected by this pattern. During our investigation we found many other independent reports. These issues in Mongo DB Compass and Mongo DB Shell are just two other examples.
Every Windows application built on Node.js with missing or optional dependencies is potentially vulnerable. This includes desktop applications that utilize Electron as well as popular web frameworks such as Next.js and React.
Each vendor has clearly stated that they will not treat these issues as vulnerabilities:
NPM’s response to our report:
“exploits that require local access to a machine are considered ineligible for npm CLI
Discord’s response to our report:
“We do not consider physical/local attacks as valid security issues”
Node.js, in the “Examples of non-vulnerabilities” section of their Security Policy:
“Node.js trusts the file system in the environment accessible to it. Therefore, it is not a vulnerability if it accesses/loads files from any path that is accessible to it.”
Conclusion
The vulnerability pattern described in this blog stems from a deliberate design decision by Node.js maintainers. While Node.js's position that “applications should trust their filesystem” may hold true on properly administered UNIX systems, it creates a systemic vulnerability on Windows where low-privileged users can write to C:\node_modules. Without a fix from Node.js, the burden silently falls on application developers.
Making matters worse, the vulnerable code may not live in the application code itself. The optional dependencies that trigger this behavior could come from third-party libraries buried in the dependency tree as we saw with both Discord and npm CLI.
We encourage security researchers to further review this issue and investigate other applications for this dangerous behavior. You can find us online at @bobbygould5 and @izobashi, and follow the team on Twitter, Mastodon, LinkedIn, or Bluesky for the latest in exploit techniques and security patches.
DISCLOSURE TIMELINES
NPM CLI:
2024-11-13 – ZDI submitted the report to the vendor
2024-11-13 – The vendor acknowledged the receipt of the report
2024-11-13 – The vendor communicated that the reported behavior was by design and they do not consider local attacks as valid security issues
2025-08-05 – ZDI encouraged the vendor to re-assess the issue
2025-12-18 – ZDI notified the vendor of the intention to publish the case as a 0-day advisory
DISCORD:
2025-07-08 – ZDI notified vendor
2025-09-11 – ZDI followed up with vendor
2025-09-15 – Vendor stated they do not consider local attacks as valid security issues
2025-12-01 – ZDI explained why we believe the issue is still valid
2025-12-10 – Vendor replied that the vulnerability is still out of scope
2025-12-11 – ZDI informed vendor of intent to publish 0-day
REFERENCES
https://nodejs.org/api/modules.html#loading-from-node_modules-folders
https://docs.npmjs.com/cli/v10/configuring-npm/package-json#optionaldependencies
https://groups.google.com/g/nodejs/c/5BGr5dliUIk/m/abJEH3sPymcJ?pli=1
https://github.com/nodejs/node-v0.x-archive/issues/8830
https://github.com/nodejs/node/security/policy#examples-of-non-vulnerabilities