Skip to content

child_process: options.shell validation #58525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3952,6 +3952,30 @@ Type: Documentation-only
The support for priority signaling has been deprecated in the [RFC 9113][], and
will be removed in future versions of Node.js.

### DEP0195: Calling `node:child_process` functions with `options.shell` as an empty string

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58525
description: Documentation-only deprecation
with `--pending-deprecation` support.
-->

Type: Documentation-only (supports [`--pending-deprecation`][])
Comment on lines +3955 to +3965
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you start with a doc-deprecation that can land earlier? Doc-only changes do not need full CI

Copy link
Contributor Author

@Renegade334 Renegade334 Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR contains the validation changes as well in a sequential commit, so would need CI anyway unless it were split out into separate PRs.

It feels like this should be a candidate for early warnings – using exec with shell: '' can completely change the execution target (eg. commands containing spaces or other metacharacters), but will equally sometimes not be noticeable, which seems spooky enough to warrant --pending-deprecation.

Copy link
Contributor

@aduh95 aduh95 Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was meaning a separate PR yes, I can open it for you if that's OK
EDIT: Done in #58564

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rebase onto that once it gets merged.


Calling the process-spawning functions with `{ shell: '' }` is almost certainly
unintentional, and can cause aberrant behavior.

To make [`child_process.execFile`][] or [`child_process.spawn`][] invoke the
default shell, use `{ shell: true }`. If the intention is not to invoke a shell
(default behavior), either omit the `shell` option, or set it to `false` or a
nullish value.

To make [`child_process.exec`][] invoke the default shell, either omit the
`shell` option, or set it to a nullish value. If the intention is not to invoke
a shell, use [`child_process.execFile`][] instead.

[DEP0142]: #dep0142-repl_builtinlibs
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
Expand Down Expand Up @@ -3980,6 +4004,7 @@ will be removed in future versions of Node.js.
[`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.exec`]: child_process.md#child_processexeccommand-options-callback
[`child_process.spawn`]: child_process.md#child_processspawncommand-args-options
[`child_process`]: child_process.md
[`clearInterval()`]: timers.md#clearintervaltimeout
Expand Down
30 changes: 23 additions & 7 deletions lib/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const {
SymbolDispose,
} = primordials;

const { getOptionValue } = require('internal/options');
const {
assignFunctionName,
convertToValidSignal,
Expand Down Expand Up @@ -197,7 +198,13 @@ function normalizeExecArgs(command, options, callback) {

// Make a shallow copy so we don't clobber the user's options object.
options = { __proto__: null, ...options };
options.shell = typeof options.shell === 'string' ? options.shell : true;

// Validate the shell, if present, otherwise request the default shell.
if (options.shell != null) {
validateString(options.shell, 'options.shell');
} else {
options.shell = true;
}

return {
file: command,
Expand Down Expand Up @@ -537,6 +544,8 @@ function copyProcessEnvToEnv(env, name, optionEnv) {
}

let emittedDEP0190Already = false;
let emittedDEP0195Already = false;
const pendingDeprecation = getOptionValue('--pending-deprecation');
function normalizeSpawnArguments(file, args, options) {
validateString(file, 'file');
validateArgumentNullCheck(file, 'file');
Expand Down Expand Up @@ -586,11 +595,19 @@ function normalizeSpawnArguments(file, args, options) {
}

// Validate the shell, if present.
if (options.shell != null &&
typeof options.shell !== 'boolean' &&
typeof options.shell !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.shell',
['boolean', 'string'], options.shell);
if (options.shell != null) {
if (typeof options.shell !== 'boolean' && typeof options.shell !== 'string') {
throw new ERR_INVALID_ARG_TYPE('options.shell',
['boolean', 'string'], options.shell);
}
validateArgumentNullCheck(options.shell, 'options.shell');
if (pendingDeprecation && options.shell === '' && !emittedDEP0195Already) {
process.emitWarning('Passing an empty string as the shell option when ' +
'calling child_process functions is deprecated.',
'DeprecationWarning',
'DEP0195');
emittedDEP0195Already = true;
}
}

// Validate argv0, if present.
Expand All @@ -612,7 +629,6 @@ function normalizeSpawnArguments(file, args, options) {
}

if (options.shell) {
validateArgumentNullCheck(options.shell, 'options.shell');
if (args.length > 0 && !emittedDEP0190Already) {
process.emitWarning(
'Passing args to a child process with shell option true can lead to security ' +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const testCopy = (shellName, shellPath) => {
const system32 = `${process.env.SystemRoot}\\System32`;

// Test CMD
test(true);
test(null);
test('cmd');
testCopy('cmd.exe', `${system32}\\cmd.exe`);
test('cmd.exe');
Expand Down
19 changes: 19 additions & 0 deletions test/parallel/test-child-process-exec-shell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Flags: --pending-deprecation
'use strict';
const common = require('../common');
const assert = require('assert');
const { exec, execSync } = require('child_process');

const invalidArgTypeError = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
};

for (const fn of [exec, execSync]) {
assert.throws(() => fn('should-throw-on-boolean-shell-option', { shell: false }), invalidArgTypeError);
}

const deprecationMessage = 'Passing an empty string as the shell option when calling ' +
'child_process functions is deprecated.';
common.expectWarning('DeprecationWarning', deprecationMessage, 'DEP0195');
exec('does-not-exist', { shell: '' }, common.mustCall());
Loading