From 4164a7b0296d99ffeabdd03adf78f4b827cab6b6 Mon Sep 17 00:00:00 2001 From: Liran Tal Date: Tue, 9 Jul 2024 14:39:30 +0300 Subject: [PATCH] child_process: fix incomplete prototype pollution hardening Prior pull request (#48726) hardened against prototype pollution vulnerabilities but effectively missed some use-cases which opened a window for prototype pollution for some child_process functions such as spawn(), spawnSync(), and execFileSync(). --- lib/child_process.js | 2 ++ ...test-child-process-prototype-tampering.mjs | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/child_process.js b/lib/child_process.js index 41206dc7eda1f7..41e8f941ccda3b 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -568,6 +568,8 @@ function normalizeSpawnArguments(file, args, options) { else validateObject(options, 'options'); + options = { __proto__: null, ...options }; + let cwd = options.cwd; // Validate the cwd, if present. diff --git a/test/parallel/test-child-process-prototype-tampering.mjs b/test/parallel/test-child-process-prototype-tampering.mjs index 5657458f911521..d94c4bdbc61621 100644 --- a/test/parallel/test-child-process-prototype-tampering.mjs +++ b/test/parallel/test-child-process-prototype-tampering.mjs @@ -1,7 +1,7 @@ import * as common from '../common/index.mjs'; import * as fixtures from '../common/fixtures.mjs'; import { EOL } from 'node:os'; -import { strictEqual } from 'node:assert'; +import { strictEqual, notStrictEqual, throws } from 'node:assert'; import cp from 'node:child_process'; // TODO(LiviaMedeiros): test on different platforms @@ -57,3 +57,35 @@ for (const tamperedUID of [0, 1, 999, 1000, 0n, 'gwak']) { delete Object.prototype.execPath; } + +for (const shellCommandArgument of ['-L && echo "tampered"']) { + Object.prototype.shell = true; + const cmd = 'pwd'; + let cmdExitCode = ''; + + const program = cp.spawn(cmd, [shellCommandArgument], { cwd: expectedCWD }); + program.stderr.on('data', common.mustCall()); + program.stdout.on('data', common.mustNotCall()); + + program.on('exit', common.mustCall((code) => { + notStrictEqual(code, 0); + })); + + cp.execFile(cmd, [shellCommandArgument], { cwd: expectedCWD }, + common.mustCall((err) => { + notStrictEqual(err.code, 0); + }) + ); + + throws(() => { + cp.execFileSync(cmd, [shellCommandArgument], { cwd: expectedCWD }); + }, (e) => { + notStrictEqual(e.status, 0); + return true; + }); + + cmdExitCode = cp.spawnSync(cmd, [shellCommandArgument], { cwd: expectedCWD }).status; + notStrictEqual(cmdExitCode, 0); + + delete Object.prototype.shell; +}