Skip to content

Commit e41a9c5

Browse files
LiviaMedeirosCeres6
authored andcommitted
child_process: harden against prototype pollution
PR-URL: nodejs#48726 Reviewed-By: Matthew Aitken <maitken033380023@gmail.com> Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 97a6bb7 commit e41a9c5

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

lib/child_process.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function fork(modulePath, args = [], options) {
138138
if (options != null) {
139139
validateObject(options, 'options');
140140
}
141-
options = { ...options, shell: false };
141+
options = { __proto__: null, ...options, shell: false };
142142
options.execPath = options.execPath || process.execPath;
143143
validateArgumentNullCheck(options.execPath, 'options.execPath');
144144

@@ -196,7 +196,7 @@ function normalizeExecArgs(command, options, callback) {
196196
}
197197

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

202202
return {
@@ -329,6 +329,7 @@ function execFile(file, args, options, callback) {
329329
({ file, args, options, callback } = normalizeExecFileArgs(file, args, options, callback));
330330

331331
options = {
332+
__proto__: null,
332333
encoding: 'utf8',
333334
timeout: 0,
334335
maxBuffer: MAX_BUFFER,
@@ -703,6 +704,7 @@ function normalizeSpawnArguments(file, args, options) {
703704

704705
return {
705706
// Make a shallow copy so we don't clobber the user's options object.
707+
__proto__: null,
706708
...options,
707709
args,
708710
cwd,
@@ -828,6 +830,7 @@ function spawn(file, args, options) {
828830
*/
829831
function spawnSync(file, args, options) {
830832
options = {
833+
__proto__: null,
831834
maxBuffer: MAX_BUFFER,
832835
...normalizeSpawnArguments(file, args, options),
833836
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as common from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import { EOL } from 'node:os';
4+
import { strictEqual } from 'node:assert';
5+
import cp from 'node:child_process';
6+
7+
// TODO(LiviaMedeiros): test on different platforms
8+
if (!common.isLinux)
9+
common.skip();
10+
11+
const expectedCWD = process.cwd();
12+
const expectedUID = process.getuid();
13+
14+
for (const tamperedCwd of ['', '/tmp', '/not/existing/malicious/path', 42n]) {
15+
Object.prototype.cwd = tamperedCwd;
16+
17+
cp.exec('pwd', common.mustSucceed((out) => {
18+
strictEqual(`${out}`, `${expectedCWD}${EOL}`);
19+
}));
20+
strictEqual(`${cp.execSync('pwd')}`, `${expectedCWD}${EOL}`);
21+
cp.execFile('pwd', common.mustSucceed((out) => {
22+
strictEqual(`${out}`, `${expectedCWD}${EOL}`);
23+
}));
24+
strictEqual(`${cp.execFileSync('pwd')}`, `${expectedCWD}${EOL}`);
25+
cp.spawn('pwd').stdout.on('data', common.mustCall((out) => {
26+
strictEqual(`${out}`, `${expectedCWD}${EOL}`);
27+
}));
28+
strictEqual(`${cp.spawnSync('pwd').stdout}`, `${expectedCWD}${EOL}`);
29+
30+
delete Object.prototype.cwd;
31+
}
32+
33+
for (const tamperedUID of [0, 1, 999, 1000, 0n, 'gwak']) {
34+
Object.prototype.uid = tamperedUID;
35+
36+
cp.exec('id -u', common.mustSucceed((out) => {
37+
strictEqual(`${out}`, `${expectedUID}${EOL}`);
38+
}));
39+
strictEqual(`${cp.execSync('id -u')}`, `${expectedUID}${EOL}`);
40+
cp.execFile('id', ['-u'], common.mustSucceed((out) => {
41+
strictEqual(`${out}`, `${expectedUID}${EOL}`);
42+
}));
43+
strictEqual(`${cp.execFileSync('id', ['-u'])}`, `${expectedUID}${EOL}`);
44+
cp.spawn('id', ['-u']).stdout.on('data', common.mustCall((out) => {
45+
strictEqual(`${out}`, `${expectedUID}${EOL}`);
46+
}));
47+
strictEqual(`${cp.spawnSync('id', ['-u']).stdout}`, `${expectedUID}${EOL}`);
48+
49+
delete Object.prototype.uid;
50+
}
51+
52+
{
53+
Object.prototype.execPath = '/not/existing/malicious/path';
54+
55+
// Does not throw ENOENT
56+
cp.fork(fixtures.path('empty.js'));
57+
58+
delete Object.prototype.execPath;
59+
}

0 commit comments

Comments
 (0)