diff --git a/doc/api/child_process.md b/doc/api/child_process.md index 0a9c0e3e326cc3..b908419076f148 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -158,7 +158,7 @@ exec('my.bat', (err, stdout, stderr) => { }); // Script with spaces in the filename: -const bat = spawn('"my script.cmd"', ['a', 'b'], { shell: true }); +const bat = spawn('"my script.cmd" a b', { shell: true }); // or: exec('"my script.cmd" a b', (err, stdout, stderr) => { // ... diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 3d3426455ae04b..95b575a9947a75 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -3840,6 +3840,20 @@ Type: Documentation-only `process.features.tls_alpn`, `process.features.tls_ocsp`, and `process.features.tls_sni` are deprecated, as their values are guaranteed to be identical to that of `process.features.tls`. +### DEP0190: Passing `args` to `child_process` `execFile`/`spawn` with `shell` option `true` + + + +Type: Runtime + +When an `args` array is passed to [`child_process.execFile`][] or [`child_process.spawn`][] with the option +`{ shell: true }`, the values are not escaped, only space-separated, which can lead to shell injection. + [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3 [RFC 8247 Section 2.4]: https://www.rfc-editor.org/rfc/rfc8247#section-2.4 @@ -3867,6 +3881,8 @@ deprecated, as their values are guaranteed to be identical to that of `process.f [`assert`]: assert.md [`asyncResource.runInAsyncScope()`]: async_context.md#asyncresourceruninasyncscopefn-thisarg-args [`buffer.subarray`]: buffer.md#bufsubarraystart-end +[`child_process.execFile`]: child_process.md#child_processexecfilefile-args-options-callback +[`child_process.spawn`]: child_process.md#child_processspawncommand-args-options [`child_process`]: child_process.md [`clearInterval()`]: timers.md#clearintervaltimeout [`clearTimeout()`]: timers.md#cleartimeouttimeout diff --git a/lib/child_process.js b/lib/child_process.js index 3fb21f755be3d7..4992c93c2353d0 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -611,6 +611,13 @@ function normalizeSpawnArguments(file, args, options) { if (options.shell) { validateArgumentNullCheck(options.shell, 'options.shell'); + if (args.length > 0) { + process.emitWarning( + 'Passing args to a child process with shell option true can lead to security ' + + 'vulnerabilities, as the arguments are not escaped, only concatenated.', + 'DeprecationWarning', + 'DEP0190'); + } const command = ArrayPrototypeJoin([file, ...args], ' '); // Set the shell, switches, and commands. if (process.platform === 'win32') { diff --git a/test/parallel/test-child-process-execfile.js b/test/parallel/test-child-process-execfile.js index c4dba6b3f9466f..69422d1ac345f0 100644 --- a/test/parallel/test-child-process-execfile.js +++ b/test/parallel/test-child-process-execfile.js @@ -47,8 +47,7 @@ const execOpts = { encoding: 'utf8', shell: true, env: { ...process.env, NODE: p { // Verify the shell option works properly execFile( - `"${common.isWindows ? execOpts.env.NODE : '$NODE'}"`, - [`"${common.isWindows ? execOpts.env.FIXTURE : '$FIXTURE'}"`, 0], + common.isWindows ? `"${execOpts.env.NODE}" "${execOpts.env.FIXTURE} 0` : `"$NODE" "$FIXTURE" 0`, execOpts, common.mustSucceed(), ); @@ -117,10 +116,14 @@ const execOpts = { encoding: 'utf8', shell: true, env: { ...process.env, NODE: p ...(common.isWindows ? [] : [{ encoding: 'utf8' }]), { shell: true, encoding: 'utf8' }, ].forEach((options) => { - const execFileSyncStdout = execFileSync(file, args, options); + const command = options.shell ? + [[file, ...args].join(' ')] : + [file, args]; + + const execFileSyncStdout = execFileSync(...command, options); assert.strictEqual(execFileSyncStdout, `foo bar${os.EOL}`); - execFile(file, args, options, common.mustCall((_, stdout) => { + execFile(...command, options, common.mustCall((_, stdout) => { assert.strictEqual(stdout, execFileSyncStdout); })); }); diff --git a/test/parallel/test-child-process-spawn-shell.js b/test/parallel/test-child-process-spawn-shell.js index a63e92f29d0adb..0fd530988c7b9f 100644 --- a/test/parallel/test-child-process-spawn-shell.js +++ b/test/parallel/test-child-process-spawn-shell.js @@ -18,6 +18,12 @@ doesNotExist.on('exit', common.mustCall((code, signal) => { })); // Verify that passing arguments works +common.expectWarning( + 'DeprecationWarning', + 'Passing args to a child process with shell option true can lead to security ' + + 'vulnerabilities, as the arguments are not escaped, only concatenated.', + 'DEP0190'); + const echo = cp.spawn('echo', ['foo'], { encoding: 'utf8', shell: true diff --git a/test/parallel/test-child-process-spawnsync-shell.js b/test/parallel/test-child-process-spawnsync-shell.js index 9db8a53366fc47..8c6f180accc2f3 100644 --- a/test/parallel/test-child-process-spawnsync-shell.js +++ b/test/parallel/test-child-process-spawnsync-shell.js @@ -19,6 +19,12 @@ else assert.strictEqual(doesNotExist.status, 127); // Exit code of /bin/sh // Verify that passing arguments works +common.expectWarning( + 'DeprecationWarning', + 'Passing args to a child process with shell option true can lead to security ' + + 'vulnerabilities, as the arguments are not escaped, only concatenated.', + 'DEP0190'); + internalCp.spawnSync = common.mustCall(function(opts) { assert.strictEqual(opts.args[opts.args.length - 1].replace(/"/g, ''), 'echo foo');