diff --git a/src/setup_node_env/harden/child_process.js b/src/setup_node_env/harden/child_process.js index c4524da367fcd6..0ca891f69f9ce9 100644 --- a/src/setup_node_env/harden/child_process.js +++ b/src/setup_node_env/harden/child_process.js @@ -30,31 +30,34 @@ function patchChildProcess(cp) { function patchOptions(hasArgs) { return function apply(target, thisArg, args) { var pos = 1; - if (pos === args.length) { + var newArgs = Object.setPrototypeOf([].concat(args), null); + + if (pos === newArgs.length) { // fn(arg1) - args[pos] = prototypelessSpawnOpts(); - } else if (pos < args.length) { - if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) { + newArgs[pos] = prototypelessSpawnOpts(); + } else if (pos < newArgs.length) { + if (hasArgs && (Array.isArray(newArgs[pos]) || newArgs[pos] == null)) { // fn(arg1, args, ...) pos++; } - if (typeof args[pos] === 'object' && args[pos] !== null) { + if (typeof newArgs[pos] === 'object' && newArgs[pos] !== null) { // fn(arg1, {}, ...) // fn(arg1, args, {}, ...) - args[pos] = prototypelessSpawnOpts(args[pos]); - } else if (args[pos] == null) { + newArgs[pos] = prototypelessSpawnOpts(newArgs[pos]); + } else if (newArgs[pos] == null) { // fn(arg1, null/undefined, ...) // fn(arg1, args, null/undefined, ...) - args[pos] = prototypelessSpawnOpts(); - } else if (typeof args[pos] === 'function') { + newArgs[pos] = prototypelessSpawnOpts(); + } else if (typeof newArgs[pos] === 'function') { // fn(arg1, callback) // fn(arg1, args, callback) - args.splice(pos, 0, prototypelessSpawnOpts()); + // `newArgs` doesn't have prototype and hence `splice` method anymore. + Array.prototype.splice.call(newArgs, pos, 0, prototypelessSpawnOpts()); } } - return target.apply(thisArg, args); + return target.apply(thisArg, newArgs); }; } diff --git a/test/harden/_node_script.js b/test/harden/_node_script.js new file mode 100644 index 00000000000000..442221706b30fc --- /dev/null +++ b/test/harden/_node_script.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +console.log('Hello from _node_script.js!'); diff --git a/test/harden/child_process.js b/test/harden/child_process.js index f15f5aceb39e75..029b1a038fcbf7 100644 --- a/test/harden/child_process.js +++ b/test/harden/child_process.js @@ -307,6 +307,50 @@ for (const name of functions) { assertProcess(t, cp.spawn(command, [], { env: { custom: 'custom' } }), { stdout: 'custom' }); }); + test('spawn(command, options) - prevent object prototype pollution', (t) => { + const pathName = path.join(__dirname, '_node_script.js'); + const options = {}; + const pollutedObject = { + env: { + NODE_OPTIONS: `--require ${pathName}`, + }, + shell: process.argv[0], + }; + // eslint-disable-next-line no-proto + options.__proto__['2'] = pollutedObject; + + const argsArray = []; + + /** + * Declares that 3 assertions should be run. + * We don't use the assertProcess function here as we need an extra assertion + * for the polluted prototype + */ + t.plan(3); + + t.deepEqual( + argsArray[2], + pollutedObject, + 'Prototype should be polluted with the object at index 2' + ); + + const stdout = ''; + + const cmd = cp.spawn(command, argsArray); + cmd.stdout.on('data', (data) => { + t.equal(data.toString().trim(), stdout); + }); + + cmd.stderr.on('data', (data) => { + t.fail(`Unexpected data on STDERR: "${data}"`); + }); + + cmd.on('close', (code) => { + t.equal(code, 0); + t.end(); + }); + }); + for (const unset of notSet) { test(`spawn(command, ${unset})`, (t) => { assertProcess(t, cp.spawn(command, unset));