Feb 17, 2026·7 min read·106 visits
Deno tried to stop command injection by blocking '.bat' and '.cmd' files. They forgot Windows is case-insensitive. By using '.BAT', attackers can trick Deno into executing batch files with unsafe arguments, leading to RCE.
A high-severity command injection vulnerability in Deno runtime on Windows, caused by an incomplete fix for the 'BatBadBut' class of bugs. Attackers could bypass extension blocklists by simply changing the case of the file extension (e.g., '.BAT' instead of '.bat'), allowing arbitrary code execution via argument injection.
Deno markets itself as the "secure by default" runtime for JavaScript and TypeScript. Unlike Node.js, it requires explicit permissions to access the network, file system, or environment. It’s a bold claim, and for the most part, the Deno team does a stellar job upholding it. But security is a chain, and that chain is often anchored to the underlying operating system. When that OS is Windows, things get weird.
This vulnerability, CVE-2026-22864, is a classic example of "developer vs. legacy compatibility." It involves the terrifyingly complex way Windows creates processes and parses arguments. Specifically, it touches on the "BatBadBut" class of vulnerabilities—a recurring nightmare where programming languages fail to account for the fact that cmd.exe handles arguments like a drunk juggler.
The core issue here isn't memory corruption or a buffer overflow. It's a logic error born from a simple misunderstanding: assuming that file.bat and file.BAT are different things. In the Unix world, they are. In the Windows world, they are identical twins, but Deno only recognized one of them as a threat.
To understand this bug, you have to understand CreateProcess on Windows. When you spawn a process pointing to a .bat or .cmd file, Windows implicitly calls cmd.exe /c your_script.bat. The problem is that cmd.exe has its own parsing rules for arguments that are completely separate from the standard C runtime parsing. Characters like &, |, and ^ are interpreted as control operators before the script even sees them.
To mitigate this, runtimes like Python, Rust, and Node have had to implement complex escaping logic. Deno, in an earlier attempt to patch this, decided to simply block the execution of batch files when certain dangerous APIs were used, or at least sanitize them heavily. They implemented a check: if the file extension is .bat or .cmd, apply strict security measures.
Here lies the flaw. The check was case-sensitive. The code looked for bat (lowercase). But the Windows filesystem (NTFS) and the process loader don't care about case. If an attacker passes payload.BAT or payload.cMd, the blocklist sees a "safe" extension, but Windows sees a batch file. The runtime hands the arguments over to the OS without the special "batch file escaping," and cmd.exe happily executes any injected commands found in those arguments.
Let's look at the Rust code responsible for this. In ext/process/lib.rs (prior to the fix), the logic was painfully simple. It treated the extension as a raw string and compared it against lowercase literals. It's the kind of code you write at 3 AM thinking, "Yeah, that covers it."
The Vulnerable Code:
// Vulnerable logic snippet
if ext_str.eq("bat") || ext_str.eq("cmd") {
// Apply security blocking or special escaping
return Err(PermissionDenied);
}This is like locking the front door but leaving the window wide open because the burglar didn't ring the doorbell. The fix, introduced in version 2.5.6 (Commit 1b07f0295dd91f393966f953099ee90505a97e50), had to implement a robust, case-insensitive check. The Deno team ended up porting the logic directly from the Rust standard library (which had already learned this lesson the hard way in CVE-2024-24576).
The Fixed Code:
// Robust, case-insensitive check using bitwise matching
let has_bat_extension = |program: &[u16]| {
matches!(
program.len().checked_sub(4).and_then(|i| program.get(i..)),
// Matches .bat, .BAT, .BaT, .cmd, .CMD, etc.
Some(
[46, 98 | 66, 97 | 65, 116 | 84] | [46, 99 | 67, 109 | 77, 100 | 68]
)
)
};Notice the bitwise | operations? That handles b (98) or B (66), a (97) or A (65), ensuring that any variation of "bat" is caught. They also updated deno_path_util to strip trailing dots and spaces, because file.bat. is also a valid batch file on Windows. It's a game of whack-a-mole.
Exploiting this is trivially easy if you can control the filename and arguments passed to Deno.Command. Imagine a scenario where a Deno application runs a helper script specified by a user or configuration file. If the attacker can point that execution to a file named with uppercase letters, they win.
Here is a functional Proof of Concept (PoC) for a vulnerable Deno version running on Windows:
// victim.ts
// Run with: deno run --allow-run victim.ts
// The attacker provides a file with an uppercase extension
const maliciousFile = "test_script.BAT";
// The attacker injects a command via arguments
// The '&' is a command separator in cmd.exe
const cmd = new Deno.Command(maliciousFile, {
args: ["& calc.exe"],
});
console.log("[+] Attempting to spawn process...");
const output = await cmd.output();The Execution Flow:
test_script.BAT. Does it end in .bat? No. It ends in .BAT..exe) and does not apply the special batch-file escaping rules.CreateProcess..BAT, invokes cmd.exe /c test_script.BAT & calc.exe.cmd.exe runs the script, hits the &, and then executes calc.exe.While the requirement to control the filename limits the surface area slightly, the impact is catastrophic when the condition is met. This is a classic Remote Code Execution (RCE) vector. If a developer uses Deno.Command to execute a user-supplied path—perhaps in a CI/CD runner, a build tool, or a server-side utility wrapper—the attacker gains full control over the Deno process's privileges.
Because Deno is often used to build modern CLI tools and backend services, this bypass effectively neutralizes the sandbox protections regarding subprocess execution. Even if you have tight --allow-run permissions, if you allow running a specific directory, and the attacker can drop a .BAT file there (or use an existing one), they can break out of the intended logic chain.
It is also a stark reminder that "sanitizing input" is impossible if you don't understand how the underlying system interprets that input. The disparity between the application's view of a file (.BAT != .bat) and the OS's view (.BAT == .bat) is a fertile ground for high-severity vulnerabilities.
The remediation is straightforward: Update to Deno v2.5.6. The patch aligns Deno's behavior with the realities of the Windows API. It now explicitly invokes %COMSPEC% (cmd.exe) with proper flags (/d /s /c) and robust argument escaping whenever any case-variation of a batch extension is detected.
For Developers:
.bat files for critical infrastructure. Use PowerShell, or better yet, write the logic in JavaScript/TypeScript directly within Deno.This vulnerability serves as a humorous, dark reminder: You might be writing modern, cutting-edge Rust and TypeScript, but underneath it all, you're still negotiating with MS-DOS ghosts from the 1980s.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Deno Deno Land | < 2.5.6 | 2.5.6 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-77 |
| Attack Vector | Network (Context Dependent) |
| CVSS Score | 8.1 (High) |
| Impact | Remote Code Execution (RCE) |
| EPSS Score | 0.00067 |
| Platform | Windows |
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.