Skip to content

Commit 162ab95

Browse files
committed
child_process: disallow args in execFile/spawn when shell option is true
This will make it throw an error when args are passed to execFile or spawn when the shell option is true. The reason for this is that when it accepts args, it gives the false impression that the args are escaped while really they are just concatenated. This makes it easy to introduce bugs and security vulnerabilities. This will break any code that relies on passing args to execFile or spawn with `{ shell: true }`. Fixes: #57143
1 parent 47cd034 commit 162ab95

6 files changed

+29
-34
lines changed

doc/api/child_process.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ exec('my.bat', (err, stdout, stderr) => {
158158
});
159159

160160
// Script with spaces in the filename:
161-
const bat = spawn('"my script.cmd"', ['a', 'b'], { shell: true });
161+
const bat = spawn('"my script.cmd" a b', { shell: true });
162162
// or:
163163
exec('"my script.cmd" a b', (err, stdout, stderr) => {
164164
// ...

lib/child_process.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,10 @@ function normalizeSpawnArguments(file, args, options) {
611611

612612
if (options.shell) {
613613
validateArgumentNullCheck(options.shell, 'options.shell');
614-
const command = ArrayPrototypeJoin([file, ...args], ' ');
614+
if (args.length > 0) {
615+
throw new ERR_INVALID_ARG_VALUE('args', args, 'must be empty when shell option is true');
616+
}
617+
const command = file;
615618
// Set the shell, switches, and commands.
616619
if (process.platform === 'win32') {
617620
if (typeof options.shell === 'string')
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
// This test ensures that execFile and spawn do not take any arguments when
4+
// shell option is set to true, as this can lead to bugs and security issues.
5+
// See https://github.com/nodejs/node/issues/57143
6+
7+
const assert = require('assert');
8+
const { execFile, execFileSync, spawn, spawnSync } = require('child_process');
9+
10+
const args = ['echo "hello', ['world"'], { shell: true }];
11+
const err = { code: 'ERR_INVALID_ARG_VALUE' };
12+
13+
assert.throws(() => execFileSync(...args), err);
14+
assert.throws(() => spawnSync(...args), err);
15+
assert.throws(() => execFile(...args), err);
16+
assert.throws(() => spawn(...args), err);

test/parallel/test-child-process-execfile.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,9 @@ const execOpts = { encoding: 'utf8', shell: true, env: { ...process.env, NODE: p
4747
{
4848
// Verify the shell option works properly
4949
execFile(
50-
`"${common.isWindows ? execOpts.env.NODE : '$NODE'}"`,
51-
[`"${common.isWindows ? execOpts.env.FIXTURE : '$FIXTURE'}"`, 0],
50+
common.isWindows ? `"${execOpts.env.NODE}" "${execOpts.env.FIXTURE} 0` : `"$NODE" "$FIXTURE" 0`,
5251
execOpts,
53-
common.mustSucceed(),
52+
common.mustSucceed()
5453
);
5554
}
5655

@@ -117,10 +116,14 @@ const execOpts = { encoding: 'utf8', shell: true, env: { ...process.env, NODE: p
117116
...(common.isWindows ? [] : [{ encoding: 'utf8' }]),
118117
{ shell: true, encoding: 'utf8' },
119118
].forEach((options) => {
120-
const execFileSyncStdout = execFileSync(file, args, options);
119+
const command = options.shell
120+
? [[file, ...args].join(' ')]
121+
: [file, args];
122+
123+
const execFileSyncStdout = execFileSync(...command, options);
121124
assert.strictEqual(execFileSyncStdout, `foo bar${os.EOL}`);
122125

123-
execFile(file, args, options, common.mustCall((_, stdout) => {
126+
execFile(...command, options, common.mustCall((_, stdout) => {
124127
assert.strictEqual(stdout, execFileSyncStdout);
125128
}));
126129
});

test/parallel/test-child-process-spawn-shell.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,6 @@ doesNotExist.on('exit', common.mustCall((code, signal) => {
1717
assert.strictEqual(code, 127); // Exit code of /bin/sh
1818
}));
1919

20-
// Verify that passing arguments works
21-
const echo = cp.spawn('echo', ['foo'], {
22-
encoding: 'utf8',
23-
shell: true
24-
});
25-
let echoOutput = '';
26-
27-
assert.strictEqual(echo.spawnargs[echo.spawnargs.length - 1].replace(/"/g, ''),
28-
'echo foo');
29-
echo.stdout.on('data', (data) => {
30-
echoOutput += data;
31-
});
32-
echo.on('close', common.mustCall((code, signal) => {
33-
assert.strictEqual(echoOutput.trim(), 'foo');
34-
}));
35-
3620
// Verify that shell features can be used
3721
const cmd = 'echo bar | cat';
3822
const command = cp.spawn(cmd, {

test/parallel/test-child-process-spawnsync-shell.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@ if (common.isWindows)
1818
else
1919
assert.strictEqual(doesNotExist.status, 127); // Exit code of /bin/sh
2020

21-
// Verify that passing arguments works
22-
internalCp.spawnSync = common.mustCall(function(opts) {
23-
assert.strictEqual(opts.args[opts.args.length - 1].replace(/"/g, ''),
24-
'echo foo');
25-
return oldSpawnSync(opts);
26-
});
27-
const echo = cp.spawnSync('echo', ['foo'], { shell: true });
28-
internalCp.spawnSync = oldSpawnSync;
29-
30-
assert.strictEqual(echo.stdout.toString().trim(), 'foo');
31-
3221
// Verify that shell features can be used
3322
const cmd = 'echo bar | cat';
3423
const command = cp.spawnSync(cmd, { shell: true });

0 commit comments

Comments
 (0)