Skip to content

Commit fc7b0dd

Browse files
committed
child_process: improve input validation
This commit applies stricter input validation in normalizeSpawnArguments(), which is run by all of the child_process methods. Additional checks are added for spawnSync() specific inputs. Fixes: #8096 Fixes: #8539 Refs: #9722 PR-URL: #8312 Reviewed-By: Ben Noordhuis <[email protected]>
1 parent b374ee8 commit fc7b0dd

3 files changed

+278
-2
lines changed

lib/child_process.js

+65-1
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ function _convertCustomFds(options) {
315315
function normalizeSpawnArguments(file /*, args, options*/) {
316316
var args, options;
317317

318+
if (typeof file !== 'string' || file.length === 0)
319+
throw new TypeError('"file" argument must be a non-empty string');
320+
318321
if (Array.isArray(arguments[1])) {
319322
args = arguments[1].slice(0);
320323
options = arguments[2];
@@ -331,6 +334,47 @@ function normalizeSpawnArguments(file /*, args, options*/) {
331334
else if (options === null || typeof options !== 'object')
332335
throw new TypeError('"options" argument must be an object');
333336

337+
// Validate the cwd, if present.
338+
if (options.cwd != null &&
339+
typeof options.cwd !== 'string') {
340+
throw new TypeError('"cwd" must be a string');
341+
}
342+
343+
// Validate detached, if present.
344+
if (options.detached != null &&
345+
typeof options.detached !== 'boolean') {
346+
throw new TypeError('"detached" must be a boolean');
347+
}
348+
349+
// Validate the uid, if present.
350+
if (options.uid != null && !Number.isInteger(options.uid)) {
351+
throw new TypeError('"uid" must be an integer');
352+
}
353+
354+
// Validate the gid, if present.
355+
if (options.gid != null && !Number.isInteger(options.gid)) {
356+
throw new TypeError('"gid" must be an integer');
357+
}
358+
359+
// Validate the shell, if present.
360+
if (options.shell != null &&
361+
typeof options.shell !== 'boolean' &&
362+
typeof options.shell !== 'string') {
363+
throw new TypeError('"shell" must be a boolean or string');
364+
}
365+
366+
// Validate argv0, if present.
367+
if (options.argv0 != null &&
368+
typeof options.argv0 !== 'string') {
369+
throw new TypeError('"argv0" must be a string');
370+
}
371+
372+
// Validate windowsVerbatimArguments, if present.
373+
if (options.windowsVerbatimArguments != null &&
374+
typeof options.windowsVerbatimArguments !== 'boolean') {
375+
throw new TypeError('"windowsVerbatimArguments" must be a boolean');
376+
}
377+
334378
// Make a shallow copy so we don't clobber the user's options object.
335379
options = Object.assign({}, options);
336380

@@ -420,13 +464,33 @@ function spawnSync(/*file, args, options*/) {
420464

421465
debug('spawnSync', opts.args, options);
422466

467+
// Validate the timeout, if present.
468+
if (options.timeout != null &&
469+
!(Number.isInteger(options.timeout) && options.timeout >= 0)) {
470+
throw new TypeError('"timeout" must be an unsigned integer');
471+
}
472+
473+
// Validate maxBuffer, if present.
474+
if (options.maxBuffer != null &&
475+
!(Number.isInteger(options.maxBuffer) && options.maxBuffer >= 0)) {
476+
throw new TypeError('"maxBuffer" must be an unsigned integer');
477+
}
478+
423479
options.file = opts.file;
424480
options.args = opts.args;
425481
options.envPairs = opts.envPairs;
426482

427-
if (options.killSignal)
483+
// Validate the kill signal, if present.
484+
if (typeof options.killSignal === 'string' ||
485+
typeof options.killSignal === 'number') {
428486
options.killSignal = lookupSignal(options.killSignal);
429487

488+
if (options.killSignal === 0)
489+
throw new RangeError('"killSignal" cannot be 0');
490+
} else if (options.killSignal != null) {
491+
throw new TypeError('"killSignal" must be a string or number');
492+
}
493+
430494
options.stdio = _validateStdio(options.stdio || 'pipe', true).stdio;
431495

432496
if (options.input) {

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const cmd = common.isWindows ? 'rundll32' : 'ls';
99
const invalidcmd = 'hopefully_you_dont_have_this_on_your_machine';
1010
const invalidArgsMsg = /Incorrect value of args option/;
1111
const invalidOptionsMsg = /"options" argument must be an object/;
12+
const invalidFileMsg =
13+
/^TypeError: "file" argument must be a non-empty string$/;
1214
const empty = common.fixturesDir + '/empty.js';
1315

1416
assert.throws(function() {
@@ -36,7 +38,16 @@ assert.doesNotThrow(function() {
3638
// verify that invalid argument combinations throw
3739
assert.throws(function() {
3840
spawn();
39-
}, /Bad argument/);
41+
}, invalidFileMsg);
42+
43+
assert.throws(function() {
44+
spawn('');
45+
}, invalidFileMsg);
46+
47+
assert.throws(function() {
48+
const file = { toString() { throw new Error('foo'); } };
49+
spawn(file);
50+
}, invalidFileMsg);
4051

4152
assert.throws(function() {
4253
spawn(cmd, null);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const spawnSync = require('child_process').spawnSync;
5+
const noop = function() {};
6+
7+
function pass(option, value) {
8+
// Run the command with the specified option. Since it's not a real command,
9+
// spawnSync() should run successfully but return an ENOENT error.
10+
const child = spawnSync('not_a_real_command', { [option]: value });
11+
12+
assert.strictEqual(child.error.code, 'ENOENT');
13+
}
14+
15+
function fail(option, value, message) {
16+
assert.throws(() => {
17+
spawnSync('not_a_real_command', { [option]: value });
18+
}, message);
19+
}
20+
21+
{
22+
// Validate the cwd option
23+
const err = /^TypeError: "cwd" must be a string$/;
24+
25+
pass('cwd', undefined);
26+
pass('cwd', null);
27+
pass('cwd', __dirname);
28+
fail('cwd', 0, err);
29+
fail('cwd', 1, err);
30+
fail('cwd', true, err);
31+
fail('cwd', false, err);
32+
fail('cwd', [], err);
33+
fail('cwd', {}, err);
34+
fail('cwd', noop, err);
35+
}
36+
37+
{
38+
// Validate the detached option
39+
const err = /^TypeError: "detached" must be a boolean$/;
40+
41+
pass('detached', undefined);
42+
pass('detached', null);
43+
pass('detached', true);
44+
pass('detached', false);
45+
fail('detached', 0, err);
46+
fail('detached', 1, err);
47+
fail('detached', __dirname, err);
48+
fail('detached', [], err);
49+
fail('detached', {}, err);
50+
fail('detached', noop, err);
51+
}
52+
53+
if (!common.isWindows) {
54+
{
55+
// Validate the uid option
56+
if (process.getuid() !== 0) {
57+
const err = /^TypeError: "uid" must be an integer$/;
58+
59+
pass('uid', undefined);
60+
pass('uid', null);
61+
pass('uid', process.getuid());
62+
fail('uid', __dirname, err);
63+
fail('uid', true, err);
64+
fail('uid', false, err);
65+
fail('uid', [], err);
66+
fail('uid', {}, err);
67+
fail('uid', noop, err);
68+
fail('uid', NaN, err);
69+
fail('uid', Infinity, err);
70+
fail('uid', 3.1, err);
71+
fail('uid', -3.1, err);
72+
}
73+
}
74+
75+
{
76+
// Validate the gid option
77+
if (process.getgid() !== 0) {
78+
const err = /^TypeError: "gid" must be an integer$/;
79+
80+
pass('gid', undefined);
81+
pass('gid', null);
82+
pass('gid', process.getgid());
83+
fail('gid', __dirname, err);
84+
fail('gid', true, err);
85+
fail('gid', false, err);
86+
fail('gid', [], err);
87+
fail('gid', {}, err);
88+
fail('gid', noop, err);
89+
fail('gid', NaN, err);
90+
fail('gid', Infinity, err);
91+
fail('gid', 3.1, err);
92+
fail('gid', -3.1, err);
93+
}
94+
}
95+
}
96+
97+
{
98+
// Validate the shell option
99+
const err = /^TypeError: "shell" must be a boolean or string$/;
100+
101+
pass('shell', undefined);
102+
pass('shell', null);
103+
pass('shell', false);
104+
fail('shell', 0, err);
105+
fail('shell', 1, err);
106+
fail('shell', [], err);
107+
fail('shell', {}, err);
108+
fail('shell', noop, err);
109+
}
110+
111+
{
112+
// Validate the argv0 option
113+
const err = /^TypeError: "argv0" must be a string$/;
114+
115+
pass('argv0', undefined);
116+
pass('argv0', null);
117+
pass('argv0', 'myArgv0');
118+
fail('argv0', 0, err);
119+
fail('argv0', 1, err);
120+
fail('argv0', true, err);
121+
fail('argv0', false, err);
122+
fail('argv0', [], err);
123+
fail('argv0', {}, err);
124+
fail('argv0', noop, err);
125+
}
126+
127+
{
128+
// Validate the windowsVerbatimArguments option
129+
const err = /^TypeError: "windowsVerbatimArguments" must be a boolean$/;
130+
131+
pass('windowsVerbatimArguments', undefined);
132+
pass('windowsVerbatimArguments', null);
133+
pass('windowsVerbatimArguments', true);
134+
pass('windowsVerbatimArguments', false);
135+
fail('windowsVerbatimArguments', 0, err);
136+
fail('windowsVerbatimArguments', 1, err);
137+
fail('windowsVerbatimArguments', __dirname, err);
138+
fail('windowsVerbatimArguments', [], err);
139+
fail('windowsVerbatimArguments', {}, err);
140+
fail('windowsVerbatimArguments', noop, err);
141+
}
142+
143+
{
144+
// Validate the timeout option
145+
const err = /^TypeError: "timeout" must be an unsigned integer$/;
146+
147+
pass('timeout', undefined);
148+
pass('timeout', null);
149+
pass('timeout', 1);
150+
pass('timeout', 0);
151+
fail('timeout', -1, err);
152+
fail('timeout', true, err);
153+
fail('timeout', false, err);
154+
fail('timeout', __dirname, err);
155+
fail('timeout', [], err);
156+
fail('timeout', {}, err);
157+
fail('timeout', noop, err);
158+
fail('timeout', NaN, err);
159+
fail('timeout', Infinity, err);
160+
fail('timeout', 3.1, err);
161+
fail('timeout', -3.1, err);
162+
}
163+
164+
{
165+
// Validate the maxBuffer option
166+
const err = /^TypeError: "maxBuffer" must be an unsigned integer$/;
167+
168+
pass('maxBuffer', undefined);
169+
pass('maxBuffer', null);
170+
pass('maxBuffer', 1);
171+
pass('maxBuffer', 0);
172+
fail('maxBuffer', 3.14, err);
173+
fail('maxBuffer', -1, err);
174+
fail('maxBuffer', NaN, err);
175+
fail('maxBuffer', Infinity, err);
176+
fail('maxBuffer', true, err);
177+
fail('maxBuffer', false, err);
178+
fail('maxBuffer', __dirname, err);
179+
fail('maxBuffer', [], err);
180+
fail('maxBuffer', {}, err);
181+
fail('maxBuffer', noop, err);
182+
}
183+
184+
{
185+
// Validate the killSignal option
186+
const typeErr = /^TypeError: "killSignal" must be a string or number$/;
187+
const rangeErr = /^RangeError: "killSignal" cannot be 0$/;
188+
const unknownSignalErr = /^Error: Unknown signal:/;
189+
190+
pass('killSignal', undefined);
191+
pass('killSignal', null);
192+
pass('killSignal', 'SIGKILL');
193+
pass('killSignal', 500);
194+
fail('killSignal', 0, rangeErr);
195+
fail('killSignal', 'SIGNOTAVALIDSIGNALNAME', unknownSignalErr);
196+
fail('killSignal', true, typeErr);
197+
fail('killSignal', false, typeErr);
198+
fail('killSignal', [], typeErr);
199+
fail('killSignal', {}, typeErr);
200+
fail('killSignal', noop, typeErr);
201+
}

0 commit comments

Comments
 (0)