Repository: denoland/deno
Pull request: fix:(ext/node): escape simple quotes in node:child_process


Changed files:
filename: ext/node/polyfills/internal/child_process.ts
patch: @@ -1284,13 +1284,15 @@ function transformDenoShellCommand(
         return a;
       })
       : result.deno_args.map((a) => {
-        // POSIX: args with shell variable refs use double quotes to
-        // preserve variable expansion. Other metacharacters use single
-        // quotes.
-        if (/\$\{[^}]+\}|\$[A-Za-z_]/.test(a)) {
-          return '"' + a.replace(/"/g, '\\"') + '"';
-        }
-        if (/[();&|<>`!\n\r\s"'\\$]/.test(a)) {
+        // POSIX shell quoting for translated args.
+        const hasShellVarRef = /\$\{[^}]+\}|\$[A-Za-z_]/.test(a);
+        const unsafeInDoubleQuotes = /`|\$\(|\\/.test(a);
+        const hasShellMetachars = /[();&|<>`!\n\r\s"'\\$]/.test(a);
+
+        if (hasShellMetachars) {
+          if (hasShellVarRef && !unsafeInDoubleQuotes) {
+            return '"' + a.replace(/"/g, '\\"') + '"';
+          }
           return "'" + a.replace(/'/g, "'\\''") + "'";
         }
         return a;

filename: tests/specs/node/child_process_shell_escape/main.out
patch: @@ -16,4 +16,12 @@ Test 8: Shell features work with string command
 PASS: Shell features work
 Test 9: Async spawn escapes args
 PASS: Async spawn injection blocked
+Test 10: Backtick + $VAR injection in args
+PASS: Backtick + $VAR injection blocked
+Test 11: $() + $VAR injection in args
+PASS: $() + $VAR injection blocked
+Test 12: Backslash-quote breakout + $VAR injection in args
+PASS: Backslash-quote breakout injection blocked
+Test 13: Backslash-quote breakout + pipe injection in args
+PASS: Backslash-quote pipe injection blocked
 All tests passed!

filename: tests/specs/node/child_process_shell_escape/main.ts
patch: @@ -121,4 +121,67 @@ await new Promise<void>((resolve) => {
   });
 });
 
+// Test 10: Backtick + $VAR injection should be blocked (CVE-2026-27190 bypass)
+// When an arg contains both $VAR and backticks, the re-quoting logic must not
+// use double quotes (which allow backtick command substitution in POSIX sh).
+console.log("Test 10: Backtick + $VAR injection in args");
+const backtickVarPayload = "`touch " + markerFile + "`$HOME";
+spawnSync(Deno.execPath(), ["eval", "''", backtickVarPayload], {
+  shell: true,
+  env: { PATH: "/usr/bin:/bin", HOME: "/tmp" },
+});
+if (fs.existsSync(markerFile)) {
+  console.log("FAIL: Backtick + $VAR injection was not blocked");
+  Deno.exit(1);
+} else {
+  console.log("PASS: Backtick + $VAR injection blocked");
+}
+
+// Test 11: $() + $VAR injection should be blocked (CVE-2026-27190 bypass)
+// Same issue with $() command substitution syntax combined with $VAR.
+console.log("Test 11: $() + $VAR injection in args");
+const subshellVarPayload = "$(touch " + markerFile + ")$HOME";
+spawnSync(Deno.execPath(), ["eval", "''", subshellVarPayload], {
+  shell: true,
+  env: { PATH: "/usr/bin:/bin", HOME: "/tmp" },
+});
+if (fs.existsSync(markerFile)) {
+  console.log("FAIL: $() + $VAR injection was not blocked");
+  Deno.exit(1);
+} else {
+  console.log("PASS: $() + $VAR injection blocked");
+}
+
+// Test 12: Backslash-quote breakout + $VAR injection should be blocked
+// A \" in the argument can break out of the double-quote re-quoting:
+// .replace(/"/g, '\\"') turns \" into \\", and the shell interprets
+// \\ as escaped backslash then " as END QUOTE. The # swallows the
+// trailing wrapper quote as a comment.
+console.log("Test 12: Backslash-quote breakout + $VAR injection in args");
+const bsQuotePayload = '$HOME\\";touch ' + markerFile + ";#";
+spawnSync(Deno.execPath(), ["eval", "''", bsQuotePayload], {
+  shell: true,
+  env: { PATH: "/usr/bin:/bin", HOME: "/tmp" },
+});
+if (fs.existsSync(markerFile)) {
+  console.log("FAIL: Backslash-quote breakout injection was not blocked");
+  Deno.exit(1);
+} else {
+  console.log("PASS: Backslash-quote breakout injection blocked");
+}
+
+// Test 13: Backslash-quote breakout with pipe injection
+console.log("Test 13: Backslash-quote breakout + pipe injection in args");
+const bsPipePayload = '$HOME\\"|touch ' + markerFile + " #";
+spawnSync(Deno.execPath(), ["eval", "''", bsPipePayload], {
+  shell: true,
+  env: { PATH: "/usr/bin:/bin", HOME: "/tmp" },
+});
+if (fs.existsSync(markerFile)) {
+  console.log("FAIL: Backslash-quote pipe injection was not blocked");
+  Deno.exit(1);
+} else {
+  console.log("PASS: Backslash-quote pipe injection blocked");
+}
+
 console.log("All tests passed!");