Skip to content

Commit b2e6048

Browse files
RaisinTenRafaelGSS
authored andcommitted
child_process: validate arguments for null bytes
This change adds validation to reject an edge case where the child_process API argument strings might contain null bytes somewhere in between. Such strings were being silently truncated before, so throwing an error should prevent misuses of this API. Fixes: #44768 Signed-off-by: Darshan Sen <[email protected]> PR-URL: #44782 Reviewed-By: James M Snell <[email protected]>
1 parent 07bf389 commit b2e6048

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed

lib/child_process.js

+28
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const {
3939
ObjectPrototypeHasOwnProperty,
4040
RegExpPrototypeExec,
4141
SafeSet,
42+
StringPrototypeIncludes,
4243
StringPrototypeSlice,
4344
StringPrototypeToUpperCase,
4445
} = primordials;
@@ -137,9 +138,11 @@ function fork(modulePath, args = [], options) {
137138
}
138139
options = { ...options, shell: false };
139140
options.execPath = options.execPath || process.execPath;
141+
validateArgumentNullCheck(options.execPath, 'options.execPath');
140142

141143
// Prepare arguments for fork:
142144
execArgv = options.execArgv || process.execArgv;
145+
validateArgumentsNullCheck(execArgv, 'options.execArgv');
143146

144147
if (execArgv === process.execArgv && process._eval != null) {
145148
const index = ArrayPrototypeLastIndexOf(execArgv, process._eval);
@@ -182,6 +185,9 @@ function _forkChild(fd, serializationMode) {
182185
}
183186

184187
function normalizeExecArgs(command, options, callback) {
188+
validateString(command, 'command');
189+
validateArgumentNullCheck(command, 'command');
190+
185191
if (typeof options === 'function') {
186192
callback = options;
187193
options = undefined;
@@ -286,6 +292,7 @@ function normalizeExecFileArgs(file, args, options, callback) {
286292
// Validate argv0, if present.
287293
if (options.argv0 != null) {
288294
validateString(options.argv0, 'options.argv0');
295+
validateArgumentNullCheck(options.argv0, 'options.argv0');
289296
}
290297

291298
return { file, args, options, callback };
@@ -536,6 +543,7 @@ function copyProcessEnvToEnv(env, name, optionEnv) {
536543

537544
function normalizeSpawnArguments(file, args, options) {
538545
validateString(file, 'file');
546+
validateArgumentNullCheck(file, 'file');
539547

540548
if (file.length === 0)
541549
throw new ERR_INVALID_ARG_VALUE('file', file, 'cannot be empty');
@@ -551,6 +559,8 @@ function normalizeSpawnArguments(file, args, options) {
551559
args = [];
552560
}
553561

562+
validateArgumentsNullCheck(args, 'args');
563+
554564
if (options === undefined)
555565
options = kEmptyObject;
556566
else
@@ -589,6 +599,7 @@ function normalizeSpawnArguments(file, args, options) {
589599
// Validate argv0, if present.
590600
if (options.argv0 != null) {
591601
validateString(options.argv0, 'options.argv0');
602+
validateArgumentNullCheck(options.argv0, 'options.argv0');
592603
}
593604

594605
// Validate windowsHide, if present.
@@ -604,6 +615,7 @@ function normalizeSpawnArguments(file, args, options) {
604615
}
605616

606617
if (options.shell) {
618+
validateArgumentNullCheck(options.shell, 'options.shell');
607619
const command = ArrayPrototypeJoin([file, ...args], ' ');
608620
// Set the shell, switches, and commands.
609621
if (process.platform === 'win32') {
@@ -681,6 +693,8 @@ function normalizeSpawnArguments(file, args, options) {
681693
for (const key of envKeys) {
682694
const value = env[key];
683695
if (value !== undefined) {
696+
validateArgumentNullCheck(key, `options.env['${key}']`);
697+
validateArgumentNullCheck(value, `options.env['${key}']`);
684698
ArrayPrototypePush(envPairs, `${key}=${value}`);
685699
}
686700
}
@@ -949,6 +963,20 @@ function execSync(command, options) {
949963
}
950964

951965

966+
function validateArgumentNullCheck(arg, propName) {
967+
if (typeof arg === 'string' && StringPrototypeIncludes(arg, '\u0000')) {
968+
throw new ERR_INVALID_ARG_VALUE(propName, arg, 'must be a string without null bytes');
969+
}
970+
}
971+
972+
973+
function validateArgumentsNullCheck(args, propName) {
974+
for (let i = 0; i < args.length; ++i) {
975+
validateArgumentNullCheck(args[i], `${propName}[${i}]`);
976+
}
977+
}
978+
979+
952980
function validateTimeout(timeout) {
953981
if (timeout != null && !(NumberIsInteger(timeout) && timeout >= 0)) {
954982
throw new ERR_OUT_OF_RANGE('timeout', 'an unsigned integer', timeout);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
'use strict';
2+
const { mustNotCall } = require('../common');
3+
4+
// Regression test for https://github.com/nodejs/node/issues/44768
5+
6+
const { throws } = require('assert');
7+
const {
8+
exec,
9+
execFile,
10+
execFileSync,
11+
execSync,
12+
fork,
13+
spawn,
14+
spawnSync,
15+
} = require('child_process');
16+
17+
// Tests for the 'command' argument
18+
19+
throws(() => exec(`${process.execPath} ${__filename} AAA BBB\0XXX CCC`, mustNotCall()), {
20+
code: 'ERR_INVALID_ARG_VALUE',
21+
message: /The argument 'command' must be a string without null bytes/
22+
});
23+
24+
throws(() => exec('BBB\0XXX AAA CCC', mustNotCall()), {
25+
code: 'ERR_INVALID_ARG_VALUE',
26+
message: /The argument 'command' must be a string without null bytes/
27+
});
28+
29+
throws(() => execSync(`${process.execPath} ${__filename} AAA BBB\0XXX CCC`), {
30+
code: 'ERR_INVALID_ARG_VALUE',
31+
message: /The argument 'command' must be a string without null bytes/
32+
});
33+
34+
throws(() => execSync('BBB\0XXX AAA CCC'), {
35+
code: 'ERR_INVALID_ARG_VALUE',
36+
message: /The argument 'command' must be a string without null bytes/
37+
});
38+
39+
// Tests for the 'file' argument
40+
41+
throws(() => spawn('BBB\0XXX'), {
42+
code: 'ERR_INVALID_ARG_VALUE',
43+
message: /The argument 'file' must be a string without null bytes/
44+
});
45+
46+
throws(() => execFile('BBB\0XXX', mustNotCall()), {
47+
code: 'ERR_INVALID_ARG_VALUE',
48+
message: /The argument 'file' must be a string without null bytes/
49+
});
50+
51+
throws(() => execFileSync('BBB\0XXX'), {
52+
code: 'ERR_INVALID_ARG_VALUE',
53+
message: /The argument 'file' must be a string without null bytes/
54+
});
55+
56+
throws(() => spawn('BBB\0XXX'), {
57+
code: 'ERR_INVALID_ARG_VALUE',
58+
message: /The argument 'file' must be a string without null bytes/
59+
});
60+
61+
throws(() => spawnSync('BBB\0XXX'), {
62+
code: 'ERR_INVALID_ARG_VALUE',
63+
message: /The argument 'file' must be a string without null bytes/
64+
});
65+
66+
// Tests for the 'modulePath' argument
67+
68+
throws(() => fork('BBB\0XXX'), {
69+
code: 'ERR_INVALID_ARG_VALUE',
70+
message: /The argument 'modulePath' must be a string or Uint8Array without null bytes/
71+
});
72+
73+
// Tests for the 'args' argument
74+
75+
// Not testing exec() and execSync() because these accept 'args' as a part of
76+
// 'command' as space-separated arguments.
77+
78+
throws(() => execFile(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC'], mustNotCall()), {
79+
code: 'ERR_INVALID_ARG_VALUE',
80+
message: /The argument 'args\[2\]' must be a string without null bytes/
81+
});
82+
83+
throws(() => execFileSync(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), {
84+
code: 'ERR_INVALID_ARG_VALUE',
85+
message: /The argument 'args\[2\]' must be a string without null bytes/
86+
});
87+
88+
throws(() => fork(__filename, ['AAA', 'BBB\0XXX', 'CCC']), {
89+
code: 'ERR_INVALID_ARG_VALUE',
90+
message: /The argument 'args\[2\]' must be a string without null bytes/
91+
});
92+
93+
throws(() => spawn(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), {
94+
code: 'ERR_INVALID_ARG_VALUE',
95+
message: /The argument 'args\[2\]' must be a string without null bytes/
96+
});
97+
98+
throws(() => spawnSync(process.execPath, [__filename, 'AAA', 'BBB\0XXX', 'CCC']), {
99+
code: 'ERR_INVALID_ARG_VALUE',
100+
message: /The argument 'args\[2\]' must be a string without null bytes/
101+
});
102+
103+
// Tests for the 'options.cwd' argument
104+
105+
throws(() => exec(process.execPath, { cwd: 'BBB\0XXX' }, mustNotCall()), {
106+
code: 'ERR_INVALID_ARG_VALUE',
107+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
108+
});
109+
110+
throws(() => execFile(process.execPath, { cwd: 'BBB\0XXX' }, mustNotCall()), {
111+
code: 'ERR_INVALID_ARG_VALUE',
112+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
113+
});
114+
115+
throws(() => execFileSync(process.execPath, { cwd: 'BBB\0XXX' }), {
116+
code: 'ERR_INVALID_ARG_VALUE',
117+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
118+
});
119+
120+
throws(() => execSync(process.execPath, { cwd: 'BBB\0XXX' }), {
121+
code: 'ERR_INVALID_ARG_VALUE',
122+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
123+
});
124+
125+
throws(() => fork(__filename, { cwd: 'BBB\0XXX' }), {
126+
code: 'ERR_INVALID_ARG_VALUE',
127+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
128+
});
129+
130+
throws(() => spawn(process.execPath, { cwd: 'BBB\0XXX' }), {
131+
code: 'ERR_INVALID_ARG_VALUE',
132+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
133+
});
134+
135+
throws(() => spawnSync(process.execPath, { cwd: 'BBB\0XXX' }), {
136+
code: 'ERR_INVALID_ARG_VALUE',
137+
message: /The property 'options\.cwd' must be a string or Uint8Array without null bytes/
138+
});
139+
140+
// Tests for the 'options.argv0' argument
141+
142+
throws(() => exec(process.execPath, { argv0: 'BBB\0XXX' }, mustNotCall()), {
143+
code: 'ERR_INVALID_ARG_VALUE',
144+
message: /The property 'options\.argv0' must be a string without null bytes/
145+
});
146+
147+
throws(() => execFile(process.execPath, { argv0: 'BBB\0XXX' }, mustNotCall()), {
148+
code: 'ERR_INVALID_ARG_VALUE',
149+
message: /The property 'options\.argv0' must be a string without null bytes/
150+
});
151+
152+
throws(() => execFileSync(process.execPath, { argv0: 'BBB\0XXX' }), {
153+
code: 'ERR_INVALID_ARG_VALUE',
154+
message: /The property 'options\.argv0' must be a string without null bytes/
155+
});
156+
157+
throws(() => execSync(process.execPath, { argv0: 'BBB\0XXX' }), {
158+
code: 'ERR_INVALID_ARG_VALUE',
159+
message: /The property 'options\.argv0' must be a string without null bytes/
160+
});
161+
162+
throws(() => fork(__filename, { argv0: 'BBB\0XXX' }), {
163+
code: 'ERR_INVALID_ARG_VALUE',
164+
message: /The property 'options\.argv0' must be a string without null bytes/
165+
});
166+
167+
throws(() => spawn(process.execPath, { argv0: 'BBB\0XXX' }), {
168+
code: 'ERR_INVALID_ARG_VALUE',
169+
message: /The property 'options\.argv0' must be a string without null bytes/
170+
});
171+
172+
throws(() => spawnSync(process.execPath, { argv0: 'BBB\0XXX' }), {
173+
code: 'ERR_INVALID_ARG_VALUE',
174+
message: /The property 'options\.argv0' must be a string without null bytes/
175+
});
176+
177+
// Tests for the 'options.shell' argument
178+
179+
throws(() => exec(process.execPath, { shell: 'BBB\0XXX' }, mustNotCall()), {
180+
code: 'ERR_INVALID_ARG_VALUE',
181+
message: /The property 'options\.shell' must be a string without null bytes/
182+
});
183+
184+
throws(() => execFile(process.execPath, { shell: 'BBB\0XXX' }, mustNotCall()), {
185+
code: 'ERR_INVALID_ARG_VALUE',
186+
message: /The property 'options\.shell' must be a string without null bytes/
187+
});
188+
189+
throws(() => execFileSync(process.execPath, { shell: 'BBB\0XXX' }), {
190+
code: 'ERR_INVALID_ARG_VALUE',
191+
message: /The property 'options\.shell' must be a string without null bytes/
192+
});
193+
194+
throws(() => execSync(process.execPath, { shell: 'BBB\0XXX' }), {
195+
code: 'ERR_INVALID_ARG_VALUE',
196+
message: /The property 'options\.shell' must be a string without null bytes/
197+
});
198+
199+
// Not testing fork() because it doesn't accept the shell option (internally it
200+
// explicitly sets shell to false).
201+
202+
throws(() => spawn(process.execPath, { shell: 'BBB\0XXX' }), {
203+
code: 'ERR_INVALID_ARG_VALUE',
204+
message: /The property 'options\.shell' must be a string without null bytes/
205+
});
206+
207+
throws(() => spawnSync(process.execPath, { shell: 'BBB\0XXX' }), {
208+
code: 'ERR_INVALID_ARG_VALUE',
209+
message: /The property 'options\.shell' must be a string without null bytes/
210+
});
211+
212+
// Tests for the 'options.env' argument
213+
214+
throws(() => exec(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }, mustNotCall()), {
215+
code: 'ERR_INVALID_ARG_VALUE',
216+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
217+
});
218+
219+
throws(() => exec(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }, mustNotCall()), {
220+
code: 'ERR_INVALID_ARG_VALUE',
221+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
222+
});
223+
224+
throws(() => execFile(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }, mustNotCall()), {
225+
code: 'ERR_INVALID_ARG_VALUE',
226+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
227+
});
228+
229+
throws(() => execFile(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }, mustNotCall()), {
230+
code: 'ERR_INVALID_ARG_VALUE',
231+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
232+
});
233+
234+
throws(() => execFileSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
235+
code: 'ERR_INVALID_ARG_VALUE',
236+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
237+
});
238+
239+
throws(() => execFileSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
240+
code: 'ERR_INVALID_ARG_VALUE',
241+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
242+
});
243+
244+
throws(() => execSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
245+
code: 'ERR_INVALID_ARG_VALUE',
246+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
247+
});
248+
249+
throws(() => execSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
250+
code: 'ERR_INVALID_ARG_VALUE',
251+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
252+
});
253+
254+
throws(() => fork(__filename, { env: { 'AAA': 'BBB\0XXX' } }), {
255+
code: 'ERR_INVALID_ARG_VALUE',
256+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
257+
});
258+
259+
throws(() => fork(__filename, { env: { 'BBB\0XXX': 'AAA' } }), {
260+
code: 'ERR_INVALID_ARG_VALUE',
261+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
262+
});
263+
264+
throws(() => spawn(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
265+
code: 'ERR_INVALID_ARG_VALUE',
266+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
267+
});
268+
269+
throws(() => spawn(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
270+
code: 'ERR_INVALID_ARG_VALUE',
271+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
272+
});
273+
274+
throws(() => spawnSync(process.execPath, { env: { 'AAA': 'BBB\0XXX' } }), {
275+
code: 'ERR_INVALID_ARG_VALUE',
276+
message: /The property 'options\.env\['AAA'\]' must be a string without null bytes/
277+
});
278+
279+
throws(() => spawnSync(process.execPath, { env: { 'BBB\0XXX': 'AAA' } }), {
280+
code: 'ERR_INVALID_ARG_VALUE',
281+
message: /The property 'options\.env\['BBB\0XXX'\]' must be a string without null bytes/
282+
});
283+
284+
// Tests for the 'options.execPath' argument
285+
throws(() => fork(__filename, { execPath: 'BBB\0XXX' }), {
286+
code: 'ERR_INVALID_ARG_VALUE',
287+
message: /The property 'options\.execPath' must be a string without null bytes/
288+
});
289+
290+
// Tests for the 'options.execArgv' argument
291+
throws(() => fork(__filename, { execArgv: ['AAA', 'BBB\0XXX', 'CCC'] }), {
292+
code: 'ERR_INVALID_ARG_VALUE',
293+
message: /The property 'options\.execArgv\[1\]' must be a string without null bytes/
294+
});

0 commit comments

Comments
 (0)