Repository: denoland/deno
Pull request: fix(node/child_process): reject null bytes in arguments

Add validation to reject null bytes (\0) in child_process arguments, matching Node.js behavior. Null bytes in command arguments can cause security issues as they may truncate strings at the OS level.

Validates null bytes in:
  - file/command argument
  - args array elements
  - options.cwd, argv0, shell, env keys/values
  - fork options: modulePath, execPath, execArgv

Also fixes execFile to pass through argv0 option to spawn.

Changed files:
filename: ext/node/polyfills/child_process.ts
patch: @@ -22,6 +22,7 @@ import {
   type SpawnSyncOptions,
   type SpawnSyncResult,
   stdioStringToArray,
+  validateNullByteNotInArg,
 } from "ext:deno_node/internal/child_process.ts";
 import {
   validateAbortSignal,
@@ -73,6 +74,7 @@ export function fork(
   _options?: ForkOptions,
 ) {
   validateString(modulePath, "modulePath");
+  validateNullByteNotInArg(modulePath, "modulePath");
 
   // Get options and args arguments.
   let execArgv;
@@ -103,6 +105,28 @@ export function fork(
     options = { ...arguments[pos++] };
   }
 
+  // Validate null bytes in args
+  for (let i = 0; i < args.length; i++) {
+    if (typeof args[i] === "string") {
+      validateNullByteNotInArg(args[i], `args[${i}]`);
+    }
+  }
+
+  // Validate null bytes in execPath
+  if (options.execPath != null) {
+    validateString(options.execPath, "options.execPath");
+    validateNullByteNotInArg(options.execPath, "options.execPath");
+  }
+
+  // Validate null bytes in execArgv
+  if (options.execArgv != null && Array.isArray(options.execArgv)) {
+    for (let i = 0; i < options.execArgv.length; i++) {
+      if (typeof options.execArgv[i] === "string") {
+        validateNullByteNotInArg(options.execArgv[i], `options.execArgv[${i}]`);
+      }
+    }
+  }
+
   // Prepare arguments for fork:
   execArgv = options.execArgv || process.execArgv;
 
@@ -493,6 +517,7 @@ export function execFile(
     );
   }
   const spawnOptions: SpawnOptions = {
+    argv0: execOptions.argv0,
     cwd: execOptions.cwd,
     env: execOptions.env,
     gid: execOptions.gid,

filename: ext/node/polyfills/internal/child_process.ts
patch: @@ -26,6 +26,7 @@ import {
   ArrayPrototypeSort,
   ArrayPrototypeUnshift,
   ObjectHasOwn,
+  StringPrototypeIncludes,
   StringPrototypeStartsWith,
   StringPrototypeToUpperCase,
 } from "ext:deno_node/internal/primordials.mjs";
@@ -690,6 +691,17 @@ function normalizeStdioOption(
   }
 }
 
+// Check for null bytes in a string and throw ERR_INVALID_ARG_VALUE if found
+export function validateNullByteNotInArg(value: string, name: string): void {
+  if (StringPrototypeIncludes(value, "\0")) {
+    throw new ERR_INVALID_ARG_VALUE(
+      name,
+      value,
+      "must be a string without null bytes",
+    );
+  }
+}
+
 export function normalizeSpawnArguments(
   file: string,
   args: string[],
@@ -701,6 +713,9 @@ export function normalizeSpawnArguments(
     throw new ERR_INVALID_ARG_VALUE("file", file, "cannot be empty");
   }
 
+  // Check for null bytes in file
+  validateNullByteNotInArg(file, "file");
+
   if (ArrayIsArray(args)) {
     args = ArrayPrototypeSlice(args);
   } else if (args == null) {
@@ -712,6 +727,14 @@ export function normalizeSpawnArguments(
     args = [];
   }
 
+  // Check for null bytes in args
+  for (let i = 0; i < args.length; i++) {
+    const arg = args[i];
+    if (typeof arg === "string") {
+      validateNullByteNotInArg(arg, `args[${i}]`);
+    }
+  }
+
   if (options === undefined) {
     options = kEmptyObject;
   } else {
@@ -723,6 +746,7 @@ export function normalizeSpawnArguments(
   // Validate the cwd, if present.
   if (cwd != null) {
     cwd = getValidatedPath(cwd, "options.cwd") as string;
+    validateNullByteNotInArg(cwd, "options.cwd");
   }
 
   // Validate detached, if present.
@@ -752,10 +776,14 @@ export function normalizeSpawnArguments(
       options.shell,
     );
   }
+  if (typeof options.shell === "string") {
+    validateNullByteNotInArg(options.shell, "options.shell");
+  }
 
   // Validate argv0, if present.
   if (options.argv0 != null) {
     validateString(options.argv0, "options.argv0");
+    validateNullByteNotInArg(options.argv0, "options.argv0");
   }
 
   // Validate windowsHide, if present.
@@ -849,6 +877,9 @@ export function normalizeSpawnArguments(
   for (const key of envKeys) {
     const value = env[key];
     if (value !== undefined) {
+      // Check for null bytes in env keys and values
+      validateNullByteNotInArg(key, `options.env['${key}']`);
+      validateNullByteNotInArg(String(value), `options.env['${key}']`);
       ArrayPrototypePush(envPairs, `${key}=${value}`);
     }
   }

filename: tests/node_compat/config.json
patch: @@ -175,6 +175,7 @@
     "parallel/test-child-process-fork3.js": {},
     "parallel/test-child-process-ipc-next-tick.js": {},
     "parallel/test-child-process-no-deprecation.js": {},
+    "parallel/test-child-process-reject-null-bytes.js": {},
     "parallel/test-child-process-send-after-close.js": {},
     "parallel/test-child-process-send-cb.js": {},
     "parallel/test-child-process-set-blocking.js": {},