High severityNVD Advisory· Published Feb 20, 2026· Updated Feb 24, 2026
Deno has a Command Injection via Incomplete shell metacharacter blocklist in node:child_process
CVE-2026-27190
Description
Deno is a JavaScript, TypeScript, and WebAssembly runtime. Prior to 2.6.8, a command injection vulnerability exists in Deno's node:child_process implementation. This vulnerability is fixed in 2.6.8.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
denocrates.io | < 2.6.8 | 2.6.8 |
Affected products
1Patches
19132ad958c83fix(ext/node): escape more shell args (#31999)
4 files changed · +201 −2
ext/node/polyfills/internal/child_process.ts+48 −2 modified@@ -968,7 +968,16 @@ export function normalizeSpawnArguments( ]); if (options.shell) { - let command = ArrayPrototypeJoin([file, ...args], " "); + // When args are provided, escape them to prevent shell injection. + // When no args are provided (just a string command), the user intends + // for shell interpretation, so don't escape. + let command; + if (args.length > 0) { + const escapedParts = [escapeShellArg(file), ...args.map(escapeShellArg)]; + command = ArrayPrototypeJoin(escapedParts, " "); + } else { + command = file; + } // Transform Node.js flags to Deno equivalents in shell commands that invoke Deno command = transformDenoShellCommand(command, options.env); // Set the shell, switches, and commands. @@ -1086,6 +1095,43 @@ function waitForStreamToClose(stream: Stream) { return deferred.promise; } +/** + * Escapes a string for safe use as a shell argument. + * On Unix, wraps in single quotes and escapes embedded single quotes. + * On Windows, wraps in double quotes and escapes embedded double quotes and backslashes. + */ +function escapeShellArg(arg: string): string { + if (process.platform === "win32") { + // Windows: use double quotes, escape double quotes and backslashes + // Empty string needs to be quoted + if (arg === "") { + return '""'; + } + // If no special characters, return as-is + if (!/[\s"\\]/.test(arg)) { + return arg; + } + // Escape backslashes before quotes, then escape quotes + let escaped = arg.replace(/(\\*)"/g, '$1$1\\"'); + // Escape trailing backslashes + escaped = escaped.replace(/(\\+)$/, "$1$1"); + return `"${escaped}"`; + } else { + // Unix: use single quotes, escape embedded single quotes + // Empty string needs to be quoted + if (arg === "") { + return "''"; + } + // If no special characters, return as-is + if (!/[^a-zA-Z0-9_./-]/.test(arg)) { + return arg; + } + // Wrap in single quotes and escape any embedded single quotes + // Single quotes are escaped by ending the string, adding an escaped quote, and starting a new string + return "'" + arg.replace(/'/g, "'\\''") + "'"; + } +} + /** * Simple shell argument splitter that handles double and single quotes. * Used to parse the arguments portion of a shell command string. @@ -1182,7 +1228,7 @@ function transformDenoShellCommand( // Check if any translated arg contains shell metacharacters (e.g. from eval // wrapping). If so, the result can't be safely used in a shell command. for (let i = 0; i < result.deno_args.length; i++) { - if (/[();&|<>`!]/.test(result.deno_args[i])) { + if (/[();&|<>`!\n\r]/.test(result.deno_args[i])) { return command; } }
tests/specs/node/child_process_shell_escape/main.out+19 −0 added@@ -0,0 +1,19 @@ +Test 1: Newline injection in args +PASS: Newline injection blocked +Test 2: Semicolon injection in args +PASS: Semicolon injection blocked +Test 3: Pipe injection in args +PASS: Pipe injection blocked +Test 4: Backtick injection in args +PASS: Backtick injection blocked +Test 5: $() injection in args +PASS: $() injection blocked +Test 6: Normal args work correctly +PASS: Normal args work +Test 7: Args with spaces preserved +PASS: Args with spaces work +Test 8: Shell features work with string command +PASS: Shell features work +Test 9: Async spawn escapes args +PASS: Async spawn injection blocked +All tests passed!
tests/specs/node/child_process_shell_escape/main.ts+124 −0 added@@ -0,0 +1,124 @@ +// Test that shell metacharacters in arguments are properly escaped +// when using shell: true with spawn/spawnSync to prevent command injection. + +import { spawn, spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +const tempDir = Deno.cwd(); +const markerFile = path.join(tempDir, "injection_marker"); + +// Clean up any existing marker file +try { + fs.unlinkSync(markerFile); +} catch { + // ignore +} + +// Test 1: Newline injection should be blocked +console.log("Test 1: Newline injection in args"); +const newlinePayload = `dummy\ntouch ${markerFile}`; +spawnSync("echo", [newlinePayload], { shell: true }); +if (fs.existsSync(markerFile)) { + console.log("FAIL: Newline injection was not blocked"); + Deno.exit(1); +} else { + console.log("PASS: Newline injection blocked"); +} + +// Test 2: Semicolon injection should be blocked +console.log("Test 2: Semicolon injection in args"); +const semicolonPayload = `dummy; touch ${markerFile}`; +spawnSync("echo", [semicolonPayload], { shell: true }); +if (fs.existsSync(markerFile)) { + console.log("FAIL: Semicolon injection was not blocked"); + Deno.exit(1); +} else { + console.log("PASS: Semicolon injection blocked"); +} + +// Test 3: Pipe injection should be blocked +console.log("Test 3: Pipe injection in args"); +const pipePayload = `dummy | touch ${markerFile}`; +spawnSync("echo", [pipePayload], { shell: true }); +if (fs.existsSync(markerFile)) { + console.log("FAIL: Pipe injection was not blocked"); + Deno.exit(1); +} else { + console.log("PASS: Pipe injection blocked"); +} + +// Test 4: Backtick injection should be blocked +console.log("Test 4: Backtick injection in args"); +const backtickPayload = "`touch " + markerFile + "`"; +spawnSync("echo", [backtickPayload], { shell: true }); +if (fs.existsSync(markerFile)) { + console.log("FAIL: Backtick injection was not blocked"); + Deno.exit(1); +} else { + console.log("PASS: Backtick injection blocked"); +} + +// Test 5: $() injection should be blocked +console.log("Test 5: $() injection in args"); +const dollarPayload = "$(touch " + markerFile + ")"; +spawnSync("echo", [dollarPayload], { shell: true }); +if (fs.existsSync(markerFile)) { + console.log("FAIL: $() injection was not blocked"); + Deno.exit(1); +} else { + console.log("PASS: $() injection blocked"); +} + +// Test 6: Normal functionality still works - args are passed correctly +console.log("Test 6: Normal args work correctly"); +const result = spawnSync("echo", ["hello", "world"], { + shell: true, + encoding: "utf-8", +}); +if (result.stdout?.trim() === "hello world") { + console.log("PASS: Normal args work"); +} else { + console.log("FAIL: Normal args broken, got:", result.stdout?.trim()); + Deno.exit(1); +} + +// Test 7: Args with spaces are preserved +console.log("Test 7: Args with spaces preserved"); +const result2 = spawnSync("echo", ["hello world"], { + shell: true, + encoding: "utf-8", +}); +if (result2.stdout?.trim() === "hello world") { + console.log("PASS: Args with spaces work"); +} else { + console.log("FAIL: Args with spaces broken, got:", result2.stdout?.trim()); + Deno.exit(1); +} + +// Test 8: Shell features work when using string command (no args) +console.log("Test 8: Shell features work with string command"); +const result3 = spawnSync("echo foo | cat", { shell: true, encoding: "utf-8" }); +if (result3.stdout?.trim() === "foo") { + console.log("PASS: Shell features work"); +} else { + console.log("FAIL: Shell features broken, got:", result3.stdout?.trim()); + Deno.exit(1); +} + +// Test 9: Async spawn also escapes args +console.log("Test 9: Async spawn escapes args"); +await new Promise<void>((resolve) => { + const child = spawn("echo", [`dummy; touch ${markerFile}`], { shell: true }); + child.on("close", () => { + if (fs.existsSync(markerFile)) { + console.log("FAIL: Async spawn injection was not blocked"); + Deno.exit(1); + } else { + console.log("PASS: Async spawn injection blocked"); + } + resolve(); + }); +}); + +console.log("All tests passed!");
tests/specs/node/child_process_shell_escape/__test__.jsonc+10 −0 added@@ -0,0 +1,10 @@ +{ + "tempDir": true, + "tests": { + "shell_args_escaped": { + "if": "unix", + "args": "run -A main.ts", + "output": "main.out" + } + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-hmh4-3xvx-q5hrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27190ghsaADVISORY
- github.com/denoland/deno/commit/9132ad958c83a0d0b199de12b69b877f63edab4cghsax_refsource_MISCWEB
- github.com/denoland/deno/releases/tag/v2.6.8ghsax_refsource_MISCWEB
- github.com/denoland/deno/security/advisories/GHSA-hmh4-3xvx-q5hrghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.