From ab586b348c825b249d005198c51672eb7724b834 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 23 Jan 2020 14:36:01 +0100 Subject: [PATCH 01/17] Harden creation of child processes Add general protection against RCE vulnerabilities similar to the one described in CVE-2019-7609. Closes #49605 --- package.json | 1 + src/setup_node_env/harden.js | 29 ++++++++++++ src/setup_node_env/index.js | 1 + src/setup_node_env/patches/child_process.js | 52 +++++++++++++++++++++ yarn.lock | 2 +- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/setup_node_env/harden.js create mode 100644 src/setup_node_env/patches/child_process.js diff --git a/package.json b/package.json index 9f12f04223103..03b38c67f6770 100644 --- a/package.json +++ b/package.json @@ -246,6 +246,7 @@ "regenerator-runtime": "^0.13.3", "regression": "2.0.1", "request": "^2.88.0", + "require-in-the-middle": "^5.0.2", "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.0", "rison-node": "1.0.2", diff --git a/src/setup_node_env/harden.js b/src/setup_node_env/harden.js new file mode 100644 index 0000000000000..fcbea9a965941 --- /dev/null +++ b/src/setup_node_env/harden.js @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var hook = require('require-in-the-middle'); + +// Ensure `process.env` doesn't inherit from `Object.prototype`. This gives +// partial protection against similar RCE vulnerabilities as described in +// CVE-2019-7609 +process.env = Object.assign(Object.create(null), process.env); + +hook(['child_process'], function(exports, name) { + return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require +}); diff --git a/src/setup_node_env/index.js b/src/setup_node_env/index.js index 897b7e617d8e1..c66027f936a32 100644 --- a/src/setup_node_env/index.js +++ b/src/setup_node_env/index.js @@ -17,6 +17,7 @@ * under the License. */ +require('./harden'); require('symbol-observable'); require('./root'); require('./node_version_validator'); diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js new file mode 100644 index 0000000000000..bee0398190e20 --- /dev/null +++ b/src/setup_node_env/patches/child_process.js @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Ensure, when spawning a new child process, that the `options` and the +// `options.env` object passed to the child process function doesn't inherit +// from `Object.prototype`. This protects against similar RCE vulnerabilities +// as described in CVE-2019-7609 +module.exports = function(cp) { + cp.exec = new Proxy(cp.exec, { apply: patchOptionsAtIndex(1) }); + cp.execFile = new Proxy(cp.execFile, { apply: patchOptionsAtIndex(2) }); + cp.fork = new Proxy(cp.fork, { apply: patchOptionsAtIndex(2) }); + cp.spawn = new Proxy(cp.spawn, { apply: patchOptionsAtIndex(2) }); + cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptionsAtIndex(2) }); + cp.execSync = new Proxy(cp.execSync, { apply: patchOptionsAtIndex(1) }); + cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptionsAtIndex(2) }); + return cp; +}; + +function patchOptionsAtIndex(index) { + return function apply(target, thisArg, argumentsList) { + argumentsList[index] = prototypelessSpawnOpts(argumentsList[index]); + return target.apply(thisArg, argumentsList); + }; +} + +function prototypelessSpawnOpts(obj) { + var prototypelessObj = Object.assign(Object.create(null), obj); + + // The `process.env` fallback has been hardened elsewhere, so here we only + // care about the case where an `env` option is provided. + if (prototypelessObj.env) { + prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env); + } + + return prototypelessObj; +} diff --git a/yarn.lock b/yarn.lock index 1cf77d50d7dbb..02188e7b1174d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25830,7 +25830,7 @@ require-from-string@^2.0.1: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-in-the-middle@^5.0.0: +require-in-the-middle@^5.0.0, require-in-the-middle@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.0.2.tgz#ce3593007a61583b39ccdcd2c167a2a326c670b2" integrity sha512-l2r6F9i6t5xp4OE9cw/daB/ooQKHZOOW1AYPADhEvk/Tj/THJDS8gePp76Zyuht6Cj57a0KL+eHK5Dyv7wZnKA== From de686eda803ff33de6b2ffafc1b0271fcb9be644 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 23 Jan 2020 14:57:30 +0100 Subject: [PATCH 02/17] Ensure process.env has the expected prototype functions --- package.json | 1 + src/setup_node_env/harden.js | 3 ++- yarn.lock | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 03b38c67f6770..507f1d61f3eaa 100644 --- a/package.json +++ b/package.json @@ -218,6 +218,7 @@ "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", + "object-prototype": "^2.0.0", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", diff --git a/src/setup_node_env/harden.js b/src/setup_node_env/harden.js index fcbea9a965941..7a0387165a573 100644 --- a/src/setup_node_env/harden.js +++ b/src/setup_node_env/harden.js @@ -18,11 +18,12 @@ */ var hook = require('require-in-the-middle'); +var ObjectPrototype = require('object-prototype'); // Ensure `process.env` doesn't inherit from `Object.prototype`. This gives // partial protection against similar RCE vulnerabilities as described in // CVE-2019-7609 -process.env = Object.assign(Object.create(null), process.env); +process.env = Object.assign(Object.create(ObjectPrototype), process.env); hook(['child_process'], function(exports, name) { return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require diff --git a/yarn.lock b/yarn.lock index 02188e7b1174d..039f59b24884e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21870,6 +21870,18 @@ object-path@0.11.4: resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= +object-prototype-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object-prototype-functions/-/object-prototype-functions-1.2.0.tgz#59ffa520f2e7ef3b3ba427ba81178eb8feca1048" + integrity sha512-7pfkHi5KHFz/B3RhQVORykDYF2CdQY/h1isJzCJJfEubKkSO59io1raYHAMUrx0Im3fUSrU1oX6E4KeCwyU/jQ== + +object-prototype@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/object-prototype/-/object-prototype-2.0.0.tgz#8173498090e6f5ad05337615147c3f330b50ba1f" + integrity sha512-hbGRLa+asxkpcg1CFd1ewOHp8P9Iyuo8AAeGJpUd/QGXJ4wloYb4RO4ZgbsWd3V+TcI9HH9aui8g7wWF45PP5Q== + dependencies: + object-prototype-functions "^1.2.0" + object-values@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/object-values/-/object-values-1.0.0.tgz#72af839630119e5b98c3b02bb8c27e3237158105" From 32c14738b055945cbe2ee9a21dd3096ee60b6a04 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 29 Jan 2020 13:17:14 +0100 Subject: [PATCH 03/17] fix: ensure process.env gets the expected prototype --- src/setup_node_env/harden.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/setup_node_env/harden.js b/src/setup_node_env/harden.js index 7a0387165a573..4027407310db9 100644 --- a/src/setup_node_env/harden.js +++ b/src/setup_node_env/harden.js @@ -18,12 +18,12 @@ */ var hook = require('require-in-the-middle'); -var ObjectPrototype = require('object-prototype'); +var create = require('object-prototype').create; // Ensure `process.env` doesn't inherit from `Object.prototype`. This gives // partial protection against similar RCE vulnerabilities as described in // CVE-2019-7609 -process.env = Object.assign(Object.create(ObjectPrototype), process.env); +process.env = Object.assign(create(), process.env); hook(['child_process'], function(exports, name) { return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require From 286b0a75f556a64fabb0b231210e6074344fa311 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 31 Jan 2020 23:14:24 +0100 Subject: [PATCH 04/17] fix errors --- package.json | 2 +- src/setup_node_env/patches/child_process.js | 46 ++++++++++++++++----- yarn.lock | 8 ++-- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 507f1d61f3eaa..83692c381347c 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", - "object-prototype": "^2.0.0", + "object-prototype": "^3.0.0", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js index bee0398190e20..90e5e9ff66b76 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/patches/child_process.js @@ -22,20 +22,44 @@ // from `Object.prototype`. This protects against similar RCE vulnerabilities // as described in CVE-2019-7609 module.exports = function(cp) { - cp.exec = new Proxy(cp.exec, { apply: patchOptionsAtIndex(1) }); - cp.execFile = new Proxy(cp.execFile, { apply: patchOptionsAtIndex(2) }); - cp.fork = new Proxy(cp.fork, { apply: patchOptionsAtIndex(2) }); - cp.spawn = new Proxy(cp.spawn, { apply: patchOptionsAtIndex(2) }); - cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptionsAtIndex(2) }); - cp.execSync = new Proxy(cp.execSync, { apply: patchOptionsAtIndex(1) }); - cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptionsAtIndex(2) }); + cp.exec = new Proxy(cp.exec, { apply: patchOptions() }); + cp.execFile = new Proxy(cp.execFile, { apply: patchOptions(true) }); + cp.fork = new Proxy(cp.fork, { apply: patchOptions(true) }); + cp.spawn = new Proxy(cp.spawn, { apply: patchOptions(true) }); + cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptions(true) }); + cp.execSync = new Proxy(cp.execSync, { apply: patchOptions() }); + cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) }); return cp; }; -function patchOptionsAtIndex(index) { - return function apply(target, thisArg, argumentsList) { - argumentsList[index] = prototypelessSpawnOpts(argumentsList[index]); - return target.apply(thisArg, argumentsList); +function patchOptions(hasArgs) { + return function apply(target, thisArg, args) { + var pos = 1; + if (pos === args.length) { + // fn(arg1) + args[pos] = Object.create(null); + } else if (pos < args.length) { + if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) { + // fn(arg1, args, ...) + pos++; + } + + if (pos < args.length && typeof args[pos] === 'object') { + // fn(arg1, {}, ...) + // fn(arg1, args, {}, ...) + args[pos] = prototypelessSpawnOpts(args[pos]); + } else if (pos < args.length && args[pos] == null) { + // fn(arg1, null, ...) + // fn(arg1, args, null, ...) + args[pos] = Object.create(null); + } else if (pos < args.length && typeof args[pos] === 'function') { + // fn(arg1, callback) + // fn(arg1, args, callback) + args.splice(pos, 0, Object.create(null)); + } + } + + return target.apply(thisArg, args); }; } diff --git a/yarn.lock b/yarn.lock index 039f59b24884e..5718291546ed8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21875,10 +21875,10 @@ object-prototype-functions@^1.2.0: resolved "https://registry.yarnpkg.com/object-prototype-functions/-/object-prototype-functions-1.2.0.tgz#59ffa520f2e7ef3b3ba427ba81178eb8feca1048" integrity sha512-7pfkHi5KHFz/B3RhQVORykDYF2CdQY/h1isJzCJJfEubKkSO59io1raYHAMUrx0Im3fUSrU1oX6E4KeCwyU/jQ== -object-prototype@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/object-prototype/-/object-prototype-2.0.0.tgz#8173498090e6f5ad05337615147c3f330b50ba1f" - integrity sha512-hbGRLa+asxkpcg1CFd1ewOHp8P9Iyuo8AAeGJpUd/QGXJ4wloYb4RO4ZgbsWd3V+TcI9HH9aui8g7wWF45PP5Q== +object-prototype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-prototype/-/object-prototype-3.0.0.tgz#ce1901633565e687179ff0e625fd18135f7f6636" + integrity sha512-68KfejzD2T9z8rIFabiZk4Zyv4mj5CisiCb7fz6nRTCSbM8t+IbVorbdkrQ8heVVeeIow+eYLS/nnFY9gH64ug== dependencies: object-prototype-functions "^1.2.0" From 8ecac04cdc3de0343d86cd3689bfe993cf60af0c Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 31 Jan 2020 23:33:58 +0100 Subject: [PATCH 05/17] add tests --- .eslintrc.js | 10 + package.json | 1 + scripts/test_hardening.js | 27 +++ tasks/config/run.js | 6 + tasks/jenkins.js | 1 + test/harden/_echo.sh | 3 + test/harden/_fork.js | 20 ++ test/harden/child_process.js | 429 +++++++++++++++++++++++++++++++++++ test/harden/process_env.js | 42 ++++ yarn.lock | 46 +++- 10 files changed, 576 insertions(+), 9 deletions(-) create mode 100644 scripts/test_hardening.js create mode 100755 test/harden/_echo.sh create mode 100644 test/harden/_fork.js create mode 100644 test/harden/child_process.js create mode 100644 test/harden/process_env.js diff --git a/.eslintrc.js b/.eslintrc.js index a678243e4f07a..b730b88a1b9cf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -514,6 +514,16 @@ module.exports = { }, }, + /** + * Harden specific rules + */ + { + files: ['test/harden/*.js'], + rules: { + 'mocha/handle-done-callback': 'off', // TODO: Find a way to disable all mocha rules + }, + }, + /** * APM overrides */ diff --git a/package.json b/package.json index 83692c381347c..b0c18daf54313 100644 --- a/package.json +++ b/package.json @@ -482,6 +482,7 @@ "strip-ansi": "^3.0.1", "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", + "tape": "^4.13.0", "tree-kill": "^1.2.2", "typescript": "3.7.2", "typings-tester": "^0.3.2", diff --git a/scripts/test_hardening.js b/scripts/test_hardening.js new file mode 100644 index 0000000000000..9db4c1b85c55f --- /dev/null +++ b/scripts/test_hardening.js @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var execFileSync = require('child_process').execFileSync; +var basename = require('path').basename; + +process.argv.slice(2).forEach(function(file) { + if (basename(file)[0] === '_') return; + console.log(process.argv[0], file); + execFileSync(process.argv[0], [file], { stdio: 'inherit' }); +}); diff --git a/tasks/config/run.js b/tasks/config/run.js index a47634a93ef14..06d1136d1d89c 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -210,6 +210,12 @@ module.exports = function(grunt) { args: ['scripts/notice', '--validate'], }), + test_hardening: scriptWithGithubChecks({ + title: 'Node.js hardening tests', + cmd: NODE, + args: ['scripts/test_hardening.js', 'test/harden/*.js'], + }), + apiIntegrationTests: scriptWithGithubChecks({ title: 'API integration tests', cmd: NODE, diff --git a/tasks/jenkins.js b/tasks/jenkins.js index b40908c9b56c3..2225abc7d04df 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -37,6 +37,7 @@ module.exports = function(grunt) { 'run:test_jest_integration', 'run:test_projects', 'run:test_karma_ci', + 'run:test_hardening', 'run:apiIntegrationTests', ]); }; diff --git a/test/harden/_echo.sh b/test/harden/_echo.sh new file mode 100755 index 0000000000000..a0114be41d1d7 --- /dev/null +++ b/test/harden/_echo.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +echo $POLLUTED$custom diff --git a/test/harden/_fork.js b/test/harden/_fork.js new file mode 100644 index 0000000000000..c088737f02e6d --- /dev/null +++ b/test/harden/_fork.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +console.log(`${process.env.POLLUTED || ''}${process.env.custom || ''}`); diff --git a/test/harden/child_process.js b/test/harden/child_process.js new file mode 100644 index 0000000000000..493bbc34101b6 --- /dev/null +++ b/test/harden/child_process.js @@ -0,0 +1,429 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../../src/setup_node_env'); + +const cp = require('child_process'); +const path = require('path'); +const test = require('tape'); + +Object.prototype.POLLUTED = 'yes'; // eslint-disable-line no-extend-native + +test.onFinish(() => { + delete Object.prototype.POLLUTED; +}); + +test('test setup ok', t => { + t.equal({}.POLLUTED, 'yes'); + t.end(); +}); + +// TODO: fork() has been omitted as it doesn't validate its arguments in +// Node.js 10 and will throw an internal error asynchronously. This is fixed in +// newer versions. +const functions = ['exec', 'execFile', 'spawn', 'execFileSync', 'execSync', 'spawnSync']; +for (const name of functions) { + test(`${name}()`, t => { + t.throws(() => cp[name](), /argument must be of type string/); + t.end(); + }); +} + +{ + const command = 'echo $POLLUTED$custom'; + + test('exec(command)', t => { + assertProcess(t, cp.exec(command)); + }); + + test('exec(command, callback)', t => { + cp.exec(command, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('exec(command, options)', t => { + assertProcess(t, cp.exec(command, {})); + }); + + test('exec(command, options) - with custom env', t => { + assertProcess(t, cp.exec(command, { env: { custom: 'custom' } }), { stdout: 'custom' }); + }); + + test('exec(command, options, callback)', t => { + cp.exec(command, {}, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('exec(command, options, callback) - with custom env', t => { + cp.exec(command, { env: { custom: 'custom' } }, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), 'custom'); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); +} + +{ + const file = path.join('test', 'harden', '_echo.sh'); + + test('execFile(file)', t => { + assertProcess(t, cp.execFile(file)); + }); + + test('execFile(file, args)', t => { + assertProcess(t, cp.execFile(file, [])); + }); + + test('execFile(file, callback)', t => { + cp.execFile(file, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('execFile(file, options)', t => { + assertProcess(t, cp.execFile(file, {})); + }); + + test('execFile(file, options) - with custom env', t => { + assertProcess(t, cp.execFile(file, { env: { custom: 'custom' } }), { stdout: 'custom' }); + }); + + test('execFile(file, options, callback)', t => { + cp.execFile(file, {}, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('execFile(file, options, callback) - with custom env', t => { + cp.execFile(file, { env: { custom: 'custom' } }, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), 'custom'); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('execFile(file, args, callback)', t => { + cp.execFile(file, [], (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('execFile(file, args, options)', t => { + assertProcess(t, cp.execFile(file, [], {})); + }); + + test('execFile(file, args, options) - with custom env', t => { + assertProcess(t, cp.execFile(file, [], { env: { custom: 'custom' } }), { stdout: 'custom' }); + }); + + test('execFile(file, args, options, callback)', t => { + cp.execFile(file, [], {}, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test('execFile(file, args, options, callback) - with custom env', t => { + cp.execFile(file, [], { env: { custom: 'custom' } }, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), 'custom'); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); +} + +{ + const modulePath = path.join('test', 'harden', '_fork.js'); + + // TODO: Forked processes don't have any stdout we can monitor without providing options + test.skip('fork(modulePath)', t => { + assertProcess(t, cp.fork(modulePath)); + }); + + // TODO: Forked processes don't have any stdout we can monitor without providing options + test.skip('execFile(file, args)', t => { + assertProcess(t, cp.fork(modulePath, [])); + }); + + test('fork(modulePath, options)', t => { + assertProcess( + t, + cp.fork(modulePath, { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }) + ); + }); + + test('fork(modulePath, options) - with custom env', t => { + assertProcess( + t, + cp.fork(modulePath, { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + env: { custom: 'custom' }, + }), + { stdout: 'custom' } + ); + }); + + test('fork(modulePath, args, options)', t => { + assertProcess( + t, + cp.fork(modulePath, [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }) + ); + }); + + test('fork(modulePath, args, options) - with custom env', t => { + assertProcess( + t, + cp.fork(modulePath, [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + env: { custom: 'custom' }, + }), + { stdout: 'custom' } + ); + }); +} + +{ + const command = path.join('test', 'harden', '_echo.sh'); + + test('spawn(command)', t => { + assertProcess(t, cp.spawn(command)); + }); + + test('spawn(command, args)', t => { + assertProcess(t, cp.spawn(command, [])); + }); + + test('spawn(command, options)', t => { + assertProcess(t, cp.spawn(command, {})); + }); + + test('spawn(command, options) - with custom env', t => { + assertProcess(t, cp.spawn(command, { env: { custom: 'custom' } }), { stdout: 'custom' }); + }); + + test('spawn(command, args, options)', t => { + assertProcess(t, cp.spawn(command, [], {})); + }); + + test('spawn(command, args, options) - with custom env', t => { + assertProcess(t, cp.spawn(command, [], { env: { custom: 'custom' } }), { stdout: 'custom' }); + }); +} + +{ + const file = path.join('test', 'harden', '_echo.sh'); + + test('execFileSync(file)', t => { + t.equal( + cp + .execFileSync(file) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test('execFileSync(file, args)', t => { + t.equal( + cp + .execFileSync(file, []) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test('execFileSync(file, options)', t => { + t.equal( + cp + .execFileSync(file, {}) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test('execFileSync(file, options) - with custom env', t => { + t.equal( + cp + .execFileSync(file, { env: { custom: 'custom' } }) + .toString() + .trim(), + 'custom' + ); + t.end(); + }); + + test('execFileSync(file, args, options)', t => { + t.equal( + cp + .execFileSync(file, [], {}) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test('execFileSync(file, args, options) - with custom env', t => { + t.equal( + cp + .execFileSync(file, [], { env: { custom: 'custom' } }) + .toString() + .trim(), + 'custom' + ); + t.end(); + }); +} + +{ + const command = 'echo $POLLUTED$custom'; + + test('execSync(command)', t => { + t.equal( + cp + .execSync(command) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test('execSync(command, options)', t => { + t.equal( + cp + .execSync(command, {}) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test('execSync(command, options) - with custom env', t => { + t.equal( + cp + .execSync(command, { env: { custom: 'custom' } }) + .toString() + .trim(), + 'custom' + ); + t.end(); + }); +} + +{ + const command = path.join('test', 'harden', '_echo.sh'); + + test('spawnSync(command)', t => { + const result = cp.spawnSync(command); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test('spawnSync(command, args)', t => { + const result = cp.spawnSync(command, []); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test('spawnSync(command, options)', t => { + const result = cp.spawnSync(command, {}); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test('spawnSync(command, options) - with custom env', t => { + const result = cp.spawnSync(command, { env: { custom: 'custom' } }); + t.error(result.error); + t.equal(result.stdout.toString().trim(), 'custom'); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test('spawnSync(command, args, options)', t => { + const result = cp.spawnSync(command, [], {}); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test('spawnSync(command, args, options) - with custom env', t => { + const result = cp.spawnSync(command, [], { env: { custom: 'custom' } }); + t.error(result.error); + t.equal(result.stdout.toString().trim(), 'custom'); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); +} + +function assertProcess(t, cmd, { stdout = '' } = {}) { + t.plan(2); + + 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(); + }); +} diff --git a/test/harden/process_env.js b/test/harden/process_env.js new file mode 100644 index 0000000000000..bdde7d2f00622 --- /dev/null +++ b/test/harden/process_env.js @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../../src/setup_node_env'); + +const test = require('tape'); +const { ObjectPrototype } = require('object-prototype'); + +test('process.env prototype', t => { + t.equal(Object.prototype.isPrototypeOf(process.env), false); + t.equal(ObjectPrototype.isPrototypeOf(process.env), true); + t.end(); +}); + +test('prototype pollution', t => { + Object.prototype.foo = 42; // eslint-disable-line no-extend-native + t.equal(process.env.foo, undefined); + delete Object.prototype.foo; + t.end(); +}); + +test('Object.prototype functions', t => { + t.equal(typeof process.env.hasOwnProperty, 'function'); + t.equal(process.env.hasOwnProperty('HOME'), true); + t.end(); +}); diff --git a/yarn.lock b/yarn.lock index 5718291546ed8..7e65165680972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11190,7 +11190,7 @@ deep-equal@^1.0.0, deep-equal@^1.0.1, deep-equal@~1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= -deep-equal@^1.1.1: +deep-equal@^1.1.1, deep-equal@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== @@ -11853,6 +11853,13 @@ dotenv@^8.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.1.0.tgz#d811e178652bfb8a1e593c6dd704ec7e90d85ea2" integrity sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA== +dotignore@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" + integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw== + dependencies: + minimatch "^3.0.4" + downgrade-root@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/downgrade-root/-/downgrade-root-1.2.2.tgz#531319715b0e81ffcc22eb28478ba27643e12c6c" @@ -12369,7 +12376,7 @@ error@^7.0.0, error@^7.0.2: string-template "~0.2.1" xtend "~4.0.0" -es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.13.0, es-abstract@^1.14.2, es-abstract@^1.17.0-next.1, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0: +es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.13.0, es-abstract@^1.14.2, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0: version "1.17.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.0.tgz#f42a517d0036a5591dbb2c463591dc8bb50309b1" integrity sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug== @@ -12386,7 +12393,7 @@ es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.13.0, es-abstract@^1.14 string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" -es-abstract@^1.15.0: +es-abstract@^1.15.0, es-abstract@^1.17.0-next.1: version "1.17.4" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== @@ -14908,7 +14915,7 @@ glob@^6.0.1, glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -16714,7 +16721,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.3: +inherits@2, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -17542,7 +17549,7 @@ is-redirect@^1.0.0: resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= -is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5: +is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== @@ -21833,7 +21840,7 @@ object-identity-map@^1.0.2: dependencies: object.entries "^1.1.0" -object-inspect@^1.7.0: +object-inspect@^1.7.0, object-inspect@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== @@ -26054,7 +26061,7 @@ resolve@^1.12.0, resolve@^1.4.0: dependencies: path-parse "^1.0.6" -resolve@^1.13.1: +resolve@^1.13.1, resolve@~1.14.2: version "1.14.2" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== @@ -27907,7 +27914,7 @@ string.prototype.padstart@^3.0.0: es-abstract "^1.4.3" function-bind "^1.0.2" -string.prototype.trim@^1.2.1: +string.prototype.trim@^1.2.1, string.prototype.trim@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782" integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw== @@ -28477,6 +28484,27 @@ tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +tape@^4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/tape/-/tape-4.13.0.tgz#e2f581ff5f12a7cbd787e9f83c76c2851782fce2" + integrity sha512-J/hvA+GJnuWJ0Sj8Z0dmu3JgMNU+MmusvkCT7+SN4/2TklW18FNCp/UuHIEhPZwHfy4sXfKYgC7kypKg4umbOw== + dependencies: + deep-equal "~1.1.1" + defined "~1.0.0" + dotignore "~0.1.2" + for-each "~0.3.3" + function-bind "~1.1.1" + glob "~7.1.6" + has "~1.0.3" + inherits "~2.0.4" + is-regex "~1.0.5" + minimist "~1.2.0" + object-inspect "~1.7.0" + resolve "~1.14.2" + resumer "~0.0.0" + string.prototype.trim "~1.2.1" + through "~2.3.8" + tape@^4.5.1: version "4.10.2" resolved "https://registry.yarnpkg.com/tape/-/tape-4.10.2.tgz#129fcf62f86df92687036a52cce7b8ddcaffd7a6" From 67e03069ff4931335daf311d64ff3ab83572b0ff Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 31 Jan 2020 23:45:03 +0100 Subject: [PATCH 06/17] ensure child_process objects inherit from ObjectPrototype --- src/setup_node_env/patches/child_process.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js index 90e5e9ff66b76..b29c411d00ae5 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/patches/child_process.js @@ -17,6 +17,11 @@ * under the License. */ +var op = require('object-prototype'); + +var create = op.create; +var assign = op.assign; + // Ensure, when spawning a new child process, that the `options` and the // `options.env` object passed to the child process function doesn't inherit // from `Object.prototype`. This protects against similar RCE vulnerabilities @@ -37,7 +42,7 @@ function patchOptions(hasArgs) { var pos = 1; if (pos === args.length) { // fn(arg1) - args[pos] = Object.create(null); + args[pos] = create(); } else if (pos < args.length) { if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) { // fn(arg1, args, ...) @@ -51,11 +56,11 @@ function patchOptions(hasArgs) { } else if (pos < args.length && args[pos] == null) { // fn(arg1, null, ...) // fn(arg1, args, null, ...) - args[pos] = Object.create(null); + args[pos] = create(); } else if (pos < args.length && typeof args[pos] === 'function') { // fn(arg1, callback) // fn(arg1, args, callback) - args.splice(pos, 0, Object.create(null)); + args.splice(pos, 0, create()); } } @@ -64,12 +69,12 @@ function patchOptions(hasArgs) { } function prototypelessSpawnOpts(obj) { - var prototypelessObj = Object.assign(Object.create(null), obj); + var prototypelessObj = assign(obj); // The `process.env` fallback has been hardened elsewhere, so here we only // care about the case where an `env` option is provided. if (prototypelessObj.env) { - prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env); + prototypelessObj.env = assign(prototypelessObj.env); } return prototypelessObj; From 8c87a624c46a5d4c62ada8b26702dbcc96aee4cc Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 31 Jan 2020 23:49:52 +0100 Subject: [PATCH 07/17] add comment about require order --- src/setup_node_env/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup_node_env/index.js b/src/setup_node_env/index.js index c66027f936a32..0f51f47572be6 100644 --- a/src/setup_node_env/index.js +++ b/src/setup_node_env/index.js @@ -17,7 +17,7 @@ * under the License. */ -require('./harden'); +require('./harden'); // this require MUST be executed before any others require('symbol-observable'); require('./root'); require('./node_version_validator'); From 2c6b84adff7cf99b0b6655b6625f1191775bcc6e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 3 Feb 2020 11:41:18 +0100 Subject: [PATCH 08/17] fix glob usage --- scripts/test_hardening.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/test_hardening.js b/scripts/test_hardening.js index 9db4c1b85c55f..88c224c4f4cff 100644 --- a/scripts/test_hardening.js +++ b/scripts/test_hardening.js @@ -19,9 +19,13 @@ var execFileSync = require('child_process').execFileSync; var basename = require('path').basename; +var glob = require('glob'); process.argv.slice(2).forEach(function(file) { - if (basename(file)[0] === '_') return; - console.log(process.argv[0], file); - execFileSync(process.argv[0], [file], { stdio: 'inherit' }); + var files = glob.sync(file); + files.forEach(function(file) { + if (basename(file)[0] === '_') return; + console.log(process.argv[0], file); + execFileSync(process.argv[0], [file], { stdio: 'inherit' }); + }); }); From f6e2fd177327103cb13f95bb71f2e8831d43c11c Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 10 Feb 2020 19:34:03 +0100 Subject: [PATCH 09/17] ensure toString.apply etc work --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b0c18daf54313..a72f953b604c9 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", - "object-prototype": "^3.0.0", + "object-prototype": "^3.0.1", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", diff --git a/yarn.lock b/yarn.lock index 7e65165680972..7c7cd958405b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21882,10 +21882,10 @@ object-prototype-functions@^1.2.0: resolved "https://registry.yarnpkg.com/object-prototype-functions/-/object-prototype-functions-1.2.0.tgz#59ffa520f2e7ef3b3ba427ba81178eb8feca1048" integrity sha512-7pfkHi5KHFz/B3RhQVORykDYF2CdQY/h1isJzCJJfEubKkSO59io1raYHAMUrx0Im3fUSrU1oX6E4KeCwyU/jQ== -object-prototype@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-prototype/-/object-prototype-3.0.0.tgz#ce1901633565e687179ff0e625fd18135f7f6636" - integrity sha512-68KfejzD2T9z8rIFabiZk4Zyv4mj5CisiCb7fz6nRTCSbM8t+IbVorbdkrQ8heVVeeIow+eYLS/nnFY9gH64ug== +object-prototype@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/object-prototype/-/object-prototype-3.0.1.tgz#d6635abf032b216241f7703e96b62d301fdb1aad" + integrity sha512-hKL+jWV9/WIzmSCAHx5jW9yUxeXcujCQmRSkQQbZlKSuFwZ+ZNRuVPChRZmg8TCD/ztLeRJsB7m5HMCTbnXbWA== dependencies: object-prototype-functions "^1.2.0" From ddb2025cd60fe18b9a404093d24c846ce6438447 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 10 Feb 2020 19:34:28 +0100 Subject: [PATCH 10/17] use commander to parse script arguments --- scripts/test_hardening.js | 35 +++++++++++++++++++++++++++-------- tasks/config/run.js | 2 +- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/scripts/test_hardening.js b/scripts/test_hardening.js index 88c224c4f4cff..03fdb596f0ab7 100644 --- a/scripts/test_hardening.js +++ b/scripts/test_hardening.js @@ -18,14 +18,33 @@ */ var execFileSync = require('child_process').execFileSync; -var basename = require('path').basename; +var readdirSync = require('fs').readdirSync; +var path = require('path'); var glob = require('glob'); +var program = require('commander'); -process.argv.slice(2).forEach(function(file) { - var files = glob.sync(file); - files.forEach(function(file) { - if (basename(file)[0] === '_') return; - console.log(process.argv[0], file); - execFileSync(process.argv[0], [file], { stdio: 'inherit' }); +program + .name('node scripts/test_hardening.js') + .arguments('[file...]') + .description( + 'Run the tests in test/harden directory. If no files are provided, all files within the directory will be run.' + ) + .action(function(files) { + if (files.length === 0) files = allFiles(); + files.forEach(function(file) { + var files = glob.sync(file); + files.forEach(function(file) { + if (path.basename(file)[0] === '_') return; + console.log(process.argv[0], file); + execFileSync(process.argv[0], [file], { stdio: 'inherit' }); + }); + }); + }) + .parse(process.argv); + +function allFiles() { + var dir = path.join('test', 'harden'); + return readdirSync(dir).map(function(file) { + return path.join(dir, file); }); -}); +} diff --git a/tasks/config/run.js b/tasks/config/run.js index 06d1136d1d89c..a1b98422af4f3 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -213,7 +213,7 @@ module.exports = function(grunt) { test_hardening: scriptWithGithubChecks({ title: 'Node.js hardening tests', cmd: NODE, - args: ['scripts/test_hardening.js', 'test/harden/*.js'], + args: ['scripts/test_hardening.js'], }), apiIntegrationTests: scriptWithGithubChecks({ From bc717a438ab20252e42ae7be0f419a55cf7e2544 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 11 Feb 2020 11:26:20 +0100 Subject: [PATCH 11/17] add tests for null/undefined args --- src/setup_node_env/patches/child_process.js | 2 +- test/harden/child_process.js | 158 ++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js index b29c411d00ae5..c3c67b36b4dec 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/patches/child_process.js @@ -49,7 +49,7 @@ function patchOptions(hasArgs) { pos++; } - if (pos < args.length && typeof args[pos] === 'object') { + if (pos < args.length && typeof args[pos] === 'object' && args[pos] !== null) { // fn(arg1, {}, ...) // fn(arg1, args, {}, ...) args[pos] = prototypelessSpawnOpts(args[pos]); diff --git a/test/harden/child_process.js b/test/harden/child_process.js index 493bbc34101b6..fb7f4b2afcfce 100644 --- a/test/harden/child_process.js +++ b/test/harden/child_process.js @@ -25,6 +25,8 @@ const test = require('tape'); Object.prototype.POLLUTED = 'yes'; // eslint-disable-line no-extend-native +const notSet = [null, undefined]; + test.onFinish(() => { delete Object.prototype.POLLUTED; }); @@ -86,6 +88,21 @@ for (const name of functions) { t.end(); }); }); + + for (const unset of notSet) { + test(`exec(command, ${unset})`, t => { + assertProcess(t, cp.exec(command, unset)); + }); + + test(`exec(command, ${unset}, callback)`, t => { + cp.exec(command, unset, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + } } { @@ -168,6 +185,38 @@ for (const name of functions) { t.end(); }); }); + + for (const unset of notSet) { + test(`execFile(file, ${unset})`, t => { + assertProcess(t, cp.execFile(file, unset)); + }); + + test(`execFile(file, ${unset}, ${unset})`, t => { + assertProcess(t, cp.execFile(file, unset, unset)); + }); + + test(`execFile(file, ${unset}, callback)`, t => { + cp.execFile(file, unset, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test(`execFile(file, ${unset}, ${unset}, callback)`, t => { + cp.execFile(file, unset, unset, (err, stdout, stderr) => { + t.error(err); + t.equal(stdout.trim(), ''); + t.equal(stderr.trim(), ''); + t.end(); + }); + }); + + test(`execFile(file, ${unset}, options)`, t => { + assertProcess(t, cp.execFile(file, unset, {})); + }); + } } { @@ -222,6 +271,27 @@ for (const name of functions) { { stdout: 'custom' } ); }); + + for (const unset of notSet) { + // TODO: Forked processes don't have any stdout we can monitor without providing options + test.skip(`fork(modulePath, ${unset})`, t => { + assertProcess(t, cp.fork(modulePath, unset)); + }); + + // TODO: Forked processes don't have any stdout we can monitor without providing options + test.skip(`fork(modulePath, ${unset}, ${unset})`, t => { + assertProcess(t, cp.fork(modulePath, unset, unset)); + }); + + test(`fork(modulePath, ${unset}, options)`, t => { + assertProcess( + t, + cp.fork(modulePath, unset, { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }) + ); + }); + } } { @@ -250,6 +320,20 @@ for (const name of functions) { test('spawn(command, args, options) - with custom env', t => { assertProcess(t, cp.spawn(command, [], { env: { custom: 'custom' } }), { stdout: 'custom' }); }); + + for (const unset of notSet) { + test(`spawn(command, ${unset})`, t => { + assertProcess(t, cp.spawn(command, unset)); + }); + + test(`spawn(command, ${unset}, ${unset})`, t => { + assertProcess(t, cp.spawn(command, unset, unset)); + }); + + test(`spawn(command, ${unset}, options)`, t => { + assertProcess(t, cp.spawn(command, unset, {})); + }); + } } { @@ -320,6 +404,41 @@ for (const name of functions) { ); t.end(); }); + + for (const unset of notSet) { + test(`execFileSync(file, ${unset})`, t => { + t.equal( + cp + .execFileSync(file, unset) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test(`execFileSync(file, ${unset}, ${unset})`, t => { + t.equal( + cp + .execFileSync(file, unset, unset) + .toString() + .trim(), + '' + ); + t.end(); + }); + + test(`execFileSync(file, ${unset}, options)`, t => { + t.equal( + cp + .execFileSync(file, unset, {}) + .toString() + .trim(), + '' + ); + t.end(); + }); + } } { @@ -357,6 +476,19 @@ for (const name of functions) { ); t.end(); }); + + for (const unset of notSet) { + test(`execSync(command, ${unset})`, t => { + t.equal( + cp + .execSync(command, unset) + .toString() + .trim(), + '' + ); + t.end(); + }); + } } { @@ -409,6 +541,32 @@ for (const name of functions) { t.equal(result.stderr.toString().trim(), ''); t.end(); }); + + for (const unset of notSet) { + test(`spawnSync(command, ${unset})`, t => { + const result = cp.spawnSync(command, unset); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test(`spawnSync(command, ${unset}, ${unset})`, t => { + const result = cp.spawnSync(command, unset, unset); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + + test(`spawnSync(command, ${unset}, options)`, t => { + const result = cp.spawnSync(command, unset, {}); + t.error(result.error); + t.equal(result.stdout.toString().trim(), ''); + t.equal(result.stderr.toString().trim(), ''); + t.end(); + }); + } } function assertProcess(t, cmd, { stdout = '' } = {}) { From cc38e366ca7916f1f0c680b4a4f9173a46030f77 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 11 Feb 2020 17:04:07 +0100 Subject: [PATCH 12/17] don't patch exec twice --- src/setup_node_env/patches/child_process.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js index c3c67b36b4dec..d1ce5150edf75 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/patches/child_process.js @@ -27,13 +27,19 @@ var assign = op.assign; // from `Object.prototype`. This protects against similar RCE vulnerabilities // as described in CVE-2019-7609 module.exports = function(cp) { - cp.exec = new Proxy(cp.exec, { apply: patchOptions() }); + // The `exec` function is currently just a wrapper around `execFile`. So for + // now there's no need to patch it. If this changes in the future, our tests + // will fail and we can comment out the line below. + // + // cp.exec = new Proxy(cp.exec, { apply: patchOptions() }); + cp.execFile = new Proxy(cp.execFile, { apply: patchOptions(true) }); cp.fork = new Proxy(cp.fork, { apply: patchOptions(true) }); cp.spawn = new Proxy(cp.spawn, { apply: patchOptions(true) }); cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptions(true) }); cp.execSync = new Proxy(cp.execSync, { apply: patchOptions() }); cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) }); + return cp; }; From 8e43fbd005c8922e63359661970a6bcd42d6b800 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 24 Feb 2020 20:07:06 +0100 Subject: [PATCH 13/17] Add external modules to packages directory --- .eslintrc.js | 10 + package.json | 2 +- .../object-prototype-functions/.npmignore | 1 + packages/object-prototype-functions/README.md | 48 ++ packages/object-prototype-functions/index.js | 40 ++ .../object-prototype-functions/package.json | 28 ++ packages/object-prototype-functions/test.js | 52 ++ packages/object-prototype/.npmignore | 1 + packages/object-prototype/README.md | 82 +++ packages/object-prototype/index.js | 146 ++++++ .../object-prototype/lib/wrap_function.js | 41 ++ packages/object-prototype/package.json | 33 ++ packages/object-prototype/test/functions.js | 23 + packages/object-prototype/test/test.js | 473 ++++++++++++++++++ packages/object-prototype/yarn.lock | 1 + yarn.lock | 12 - 16 files changed, 980 insertions(+), 13 deletions(-) create mode 100644 packages/object-prototype-functions/.npmignore create mode 100644 packages/object-prototype-functions/README.md create mode 100644 packages/object-prototype-functions/index.js create mode 100644 packages/object-prototype-functions/package.json create mode 100644 packages/object-prototype-functions/test.js create mode 100644 packages/object-prototype/.npmignore create mode 100644 packages/object-prototype/README.md create mode 100644 packages/object-prototype/index.js create mode 100644 packages/object-prototype/lib/wrap_function.js create mode 100644 packages/object-prototype/package.json create mode 100644 packages/object-prototype/test/functions.js create mode 100644 packages/object-prototype/test/test.js create mode 120000 packages/object-prototype/yarn.lock diff --git a/.eslintrc.js b/.eslintrc.js index b730b88a1b9cf..cc07f02d897e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -524,6 +524,16 @@ module.exports = { }, }, + /** + * object-prototype package specific rules + */ + { + files: ['packages/object-prototype/**/*.js'], + rules: { + 'mocha/handle-done-callback': 'off', // TODO: Find a way to disable all mocha rules + }, + }, + /** * APM overrides */ diff --git a/package.json b/package.json index a72f953b604c9..327c41600f107 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", - "object-prototype": "^3.0.1", + "object-prototype": "3.0.4", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", diff --git a/packages/object-prototype-functions/.npmignore b/packages/object-prototype-functions/.npmignore new file mode 100644 index 0000000000000..daa602947e471 --- /dev/null +++ b/packages/object-prototype-functions/.npmignore @@ -0,0 +1 @@ +test.js diff --git a/packages/object-prototype-functions/README.md b/packages/object-prototype-functions/README.md new file mode 100644 index 0000000000000..e0cca078a185b --- /dev/null +++ b/packages/object-prototype-functions/README.md @@ -0,0 +1,48 @@ +# object-prototype-functions + +An array containing the names of the functions on `Object.prototype`. + +[![npm](https://img.shields.io/npm/v/object-prototype-functions.svg)](https://www.npmjs.com/package/object-prototype-functions) + +## Installation + +``` +npm install object-prototype-functions --save +``` + +## Usage + +```js +const ObjectPrototypeFunctions = require('object-prototype-functions') + +console.log('The functions of Object.prototype are:') +console.log(ObjectPrototypeFunctions.join(', ')) // hasOwnProperty, isPrototypeOf... +``` + +## API + +### `ObjectPrototypeFunctions` + +An array containing the names of the functions on `Object.prototype`. + +### `ObjectPrototypeFunctions.deprecated` + +An array containing the names of the deprecated functions on +`Object.prototype`. + +### `ObjectPrototypeFunctions.nonSpec` + +An array containing the names of the non-spec'd functions on +`Object.prototype`. + +### `ObjectPrototypeFunctions.nodejs` + +An array containing the all names of the functions on `Object.prototype` +that are normally available in Node.js - both the spec'd and deprecated +functions. + +### `ObjectPrototypeFunctions.all` + +An array containing the all names of the functions on +`Object.prototype`, both the spec'd, deprecated, and non-spec'd +functions. diff --git a/packages/object-prototype-functions/index.js b/packages/object-prototype-functions/index.js new file mode 100644 index 0000000000000..07e6ee1f8fc69 --- /dev/null +++ b/packages/object-prototype-functions/index.js @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = [ + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf', +]; + +module.exports.deprecated = [ + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__', +]; + +module.exports.nonSpec = ['eval', 'unwatch', 'watch']; + +module.exports.nodejs = module.exports.concat(module.exports.deprecated); + +module.exports.all = module.exports.concat(module.exports.deprecated, module.exports.nonSpec); diff --git a/packages/object-prototype-functions/package.json b/packages/object-prototype-functions/package.json new file mode 100644 index 0000000000000..e2b423d4225de --- /dev/null +++ b/packages/object-prototype-functions/package.json @@ -0,0 +1,28 @@ +{ + "name": "object-prototype-functions", + "version": "1.2.0", + "description": "An array containing the names of the functions on `Object.prototype`", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/elastic/kibana.git" + }, + "keywords": [ + "object", + "prototype", + "function", + "functions", + "name" + ], + "author": "Thomas Watson (https://twitter.com/wa7son)", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/elastic/kibana/issues" + }, + "homepage": "https://github.com/elastic/kibana/blob/master/packages/object-prototype-functions/README.md" +} diff --git a/packages/object-prototype-functions/test.js b/packages/object-prototype-functions/test.js new file mode 100644 index 0000000000000..c6483a50a9f8f --- /dev/null +++ b/packages/object-prototype-functions/test.js @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const assert = require('assert'); +const ObjectPrototypeFunctions = require('./'); + +assert(Array.isArray(ObjectPrototypeFunctions)); +assert(ObjectPrototypeFunctions.length > 0); + +ObjectPrototypeFunctions.forEach(name => { + assert.strictEqual(typeof Object.prototype[name], 'function'); +}); + +assert(Array.isArray(ObjectPrototypeFunctions.deprecated)); +assert(ObjectPrototypeFunctions.deprecated.length > 0); + +ObjectPrototypeFunctions.deprecated.forEach(name => { + assert.strictEqual(typeof Object.prototype[name], 'function'); +}); + +assert(Array.isArray(ObjectPrototypeFunctions.nonSpec)); +assert(ObjectPrototypeFunctions.nonSpec.length > 0); + +assert(Array.isArray(ObjectPrototypeFunctions.nodejs)); +assert.strictEqual( + ObjectPrototypeFunctions.nodejs.length, + ObjectPrototypeFunctions.length + ObjectPrototypeFunctions.deprecated.length +); + +assert(Array.isArray(ObjectPrototypeFunctions.all)); +assert.strictEqual( + ObjectPrototypeFunctions.all.length, + ObjectPrototypeFunctions.length + + ObjectPrototypeFunctions.deprecated.length + + ObjectPrototypeFunctions.nonSpec.length +); diff --git a/packages/object-prototype/.npmignore b/packages/object-prototype/.npmignore new file mode 100644 index 0000000000000..9daeafb9864cf --- /dev/null +++ b/packages/object-prototype/.npmignore @@ -0,0 +1 @@ +test diff --git a/packages/object-prototype/README.md b/packages/object-prototype/README.md new file mode 100644 index 0000000000000..2a95086667537 --- /dev/null +++ b/packages/object-prototype/README.md @@ -0,0 +1,82 @@ +# object-prototype + +A replacement prototype for `Object.prototype` with all the same +functions. + +[![npm](https://img.shields.io/npm/v/object-prototype.svg)](https://www.npmjs.com/package/object-prototype) + +## Installation + +``` +npm install object-prototype --save +``` + +## Usage + +```js +const { create, ObjectPrototype } = require('object-prototype'); + +const obj1 = create(); +const obj2 = {}; + +console.log(Object.prototype.isPrototypeOf(obj1)); // false +console.log(ObjectPrototype.isPrototypeOf(obj1)); // true + +console.log(Object.prototype.isPrototypeOf(obj2)); // true +console.log(ObjectPrototype.isPrototypeOf(obj2)); // false + +Object.prototype.foo = 42; + +console.log(obj1.foo); // undefined +console.log(obj2.foo); // 42 +``` + +## API + +### `ObjectPrototype` + +The `ObjectPrototype` property exposed by this module is ment as a +replacement to `Object.prototype` and has the following ECMAScript +spec'd functions: + +- [`ObjectPrototype.hasOwnProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) +- [`ObjectPrototype.isPrototypeOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf) +- [`ObjectPrototype.propertyIsEnumerable()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable) +- [`ObjectPrototype.toLocaleString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toLocaleString) +- [`ObjectPrototype.toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) +- [`ObjectPrototype.valueOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf) + +And the following functions which are considered deprecated according to +the ECMAScript specification: + +- [`ObjectPrototype.__defineGetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__) +- [`ObjectPrototype.__defineSetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineSetter__) +- [`ObjectPrototype.__lookupGetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__lookupGetter__) +- [`ObjectPrototype.__lookupSetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__lookupSetter__) + +### `object = create()` + +The `create` function is a convenience function that returns a new +object with `ObjectPrototype` as its prototype. + +This is equivalent to writing `Object.create(ObjectPrototype)`. + +### `object = assign([...objects])` + +The `assign` function is a convenience function that returns a new +object with all the same properties as the provided objects, but with +`ObjectPrototype` as its prototype. + +This is equivalent to writing +`Object.assign(Object.create(ObjectPrototype), ...objects)`. + +### `FunctionPrototype` + +The `FunctionPrototype` property exposed by this module is ment as a +replacement to `Function.prototype` and exposes the same properties. + +### `function = safePrototypeFunction(function)` + +Given a function as the first argument, `safePrototypeFunction` will +return another function which wraps the given function in a way so that +it doesn't leak `Object.prototype`. diff --git a/packages/object-prototype/index.js b/packages/object-prototype/index.js new file mode 100644 index 0000000000000..ac7cc93716f11 --- /dev/null +++ b/packages/object-prototype/index.js @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const functions = require('object-prototype-functions').nodejs; +const wrap = require('./lib/wrap_function'); + +const ObjectPrototype = Object.create(null); +const FunctionPrototype = Object.create(ObjectPrototype); + +exports.ObjectPrototype = ObjectPrototype; +exports.create = () => Object.create(ObjectPrototype); +exports.assign = (...args) => Object.assign(exports.create(), ...args); + +exports.FunctionPrototype = FunctionPrototype; +exports.safePrototypeFunction = safePrototypeFunction; + +/** + * ObjectPrototype + */ + +const descriptors = { + ['__proto__']: { + get: safePrototypeFunction(function() { + 'use strict'; // eslint-disable-line strict + return Object.getPrototypeOf(this); + }), + set: safePrototypeFunction(function(proto) { + 'use strict'; // eslint-disable-line strict + Object.setPrototypeOf(this, proto); + }), + enumerable: false, + configurable: true, + }, + constructor: { + value: undefined, // NOTE: This doesn't try to mimic the usual Object behavior + writable: true, + enumerable: false, + configurable: true, + }, +}; + +for (const name of functions) { + const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, name); + descriptor.value = safePrototypeFunction(descriptor.value); + descriptors[name] = descriptor; +} + +Object.defineProperties(ObjectPrototype, descriptors); + +/** + * FunctionPrototype + */ + +Object.defineProperties(FunctionPrototype, { + length: { value: 0, writable: false, enumerable: false, configurable: true }, + name: { value: '', writable: false, enumerable: false, configurable: true }, + arguments: { + get: safePrototypeFunction(Function.prototype.__lookupGetter__('arguments')), + set: safePrototypeFunction(Function.prototype.__lookupSetter__('arguments')), + enumerable: false, + configurable: true, + }, + caller: { + get: safePrototypeFunction(Function.prototype.__lookupGetter__('caller')), + set: safePrototypeFunction(Function.prototype.__lookupSetter__('caller')), + enumerable: false, + configurable: true, + }, + constructor: { + value: undefined, // NOTE: This doesn't try to mimic the usual Object behavior + writable: true, + enumerable: false, + configurable: true, + }, + apply: { + value: safePrototypeFunction(Function.prototype.apply), + writable: true, + enumerable: false, + configurable: true, + }, + bind: { + value: safePrototypeFunction(function bind() { + 'use strict'; // eslint-disable-line strict + const bound = Function.prototype.bind.apply(this, arguments); + Object.setPrototypeOf(bound, FunctionPrototype); + return bound; + }), + writable: true, + enumerable: false, + configurable: true, + }, + call: { + value: safePrototypeFunction(Function.prototype.call), + writable: true, + enumerable: false, + configurable: true, + }, + toString: { + value: safePrototypeFunction(function toString() { + 'use strict'; // eslint-disable-line strict + // TODO: Should we really do this? + return `function ${this.name}() { [native code] }`; + // Alternatively we expose the normal behavior: + // return Function.prototype.toString.apply(this, arguments) + }), + writable: true, + enumerable: false, + configurable: true, + }, + + // NOTE: I skipped overriding Function.prototype[Symbol.hasInstance] as + // this function would only be called if `FunctionPrototype` is on the + // right hand side of the `instaceof` operator. +}); + +function safePrototypeFunction(original, name) { + const wrapped = wrap(name || original.name, original); + + // In strict mode, the expected descriptors are: length, name, prototype + // In non-strict mode, the expected descriptors are: length, name, arguments, caller, prototype + const descriptors = Object.getOwnPropertyDescriptors(original); + delete descriptors.name; // will be set automatally when the function is created below + delete descriptors.prototype; // we'll use the `wrapped` functions own `prototype` property + + Object.defineProperties(wrapped, descriptors); + Object.setPrototypeOf(wrapped, FunctionPrototype); + Object.setPrototypeOf(wrapped.prototype, ObjectPrototype); + + return wrapped; +} diff --git a/packages/object-prototype/lib/wrap_function.js b/packages/object-prototype/lib/wrap_function.js new file mode 100644 index 0000000000000..8c3ff66514808 --- /dev/null +++ b/packages/object-prototype/lib/wrap_function.js @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = function(name, fn) { + if (isSloppy(fn)) { + // Hack to give the function the same name as the original + return { + [name]: function() { + return fn.apply(this, arguments); + }, + }[name]; + } else { + // Hack to give the function the same name as the original + return { + [name]: function() { + 'use strict'; // eslint-disable-line strict + return fn.apply(this, arguments); + }, + }[name]; + } +}; + +function isSloppy(fn) { + return Object.prototype.hasOwnProperty.call(fn, 'caller'); +} diff --git a/packages/object-prototype/package.json b/packages/object-prototype/package.json new file mode 100644 index 0000000000000..547b8c146cdcf --- /dev/null +++ b/packages/object-prototype/package.json @@ -0,0 +1,33 @@ +{ + "name": "object-prototype", + "version": "3.0.4", + "description": "A replacement prototype for `Object.prototype` with all the same functions", + "main": "index.js", + "dependencies": { + "object-prototype-functions": "1.2.0" + }, + "devDependencies": { + "debug": "^4.1.1", + "tape": "^4.13.0" + }, + "scripts": { + "test": "node test/test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/elastic/kibana.git" + }, + "keywords": [ + "object", + "prototype", + "pollution", + "polyfill", + "security" + ], + "author": "Thomas Watson (https://twitter.com/wa7son)", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/elastic/kibana/issues" + }, + "homepage": "https://github.com/elastic/kibana/blob/master/packages/object-prototype/README.md" +} diff --git a/packages/object-prototype/test/functions.js b/packages/object-prototype/test/functions.js new file mode 100644 index 0000000000000..385224455f8e7 --- /dev/null +++ b/packages/object-prototype/test/functions.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +exports.sloppy = function() {}; +exports.strict = function() { + 'use strict'; // eslint-disable-line strict +}; diff --git a/packages/object-prototype/test/test.js b/packages/object-prototype/test/test.js new file mode 100644 index 0000000000000..6f961c1745eac --- /dev/null +++ b/packages/object-prototype/test/test.js @@ -0,0 +1,473 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable no-prototype-builtins, no-proto */ + +const test = require('tape'); +const debug = require('debug')('tests'); +const functions = require('object-prototype-functions').nodejs; +const { sloppy, strict } = require('./functions'); +const { + create, + assign, + ObjectPrototype, + FunctionPrototype, + safePrototypeFunction, +} = require('../'); + +test('isSloppy', function(t) { + t.equal(isSloppy(sloppy), true); + t.equal(isSloppy(strict), false); + t.end(); +}); + +test('doesObjectLeak', function(t) { + t.ok(doesObjectLeak(function() {})); + t.ok(doesObjectLeak(() => {})); + t.ok(doesObjectLeak({})); + t.ok(doesObjectLeak(Object)); + t.ok(doesObjectLeak(Function)); + t.ok(doesObjectLeak(Object.prototype)); + t.ok(doesObjectLeak(Function.prototype)); + t.notOk(doesObjectLeak(Object.create(null))); + t.end(); +}); + +test('safePrototypeFunction maintains strictness', function(t) { + t.equal(isSloppy(safePrototypeFunction(sloppy)), true); + t.equal(isSloppy(safePrototypeFunction(strict)), false); + t.end(); +}); + +test("safePrototypeFunction doesn't leak", function(t) { + const fn = safePrototypeFunction(function() {}); + assertNoLeak(t, fn); + t.end(); +}); + +test('FunctionPrototype.__proto__', function(t) { + t.equal(FunctionPrototype.__proto__, ObjectPrototype); + t.end(); +}); + +test('FunctionPrototype.length', function(t) { + t.equal(Function.prototype.length, 0); + t.equal(FunctionPrototype.length, 0); + + t.equal((() => {}).length, 0); + t.equal(((a, b) => {}).length, 2); // eslint-disable-line no-unused-vars + t.equal(function() {}.length, 0); + t.equal(function(a, b) {}.length, 2); // eslint-disable-line no-unused-vars + + t.equal(safePrototypeFunction(() => {}).length, 0); + t.equal(safePrototypeFunction((a, b) => {}).length, 2); // eslint-disable-line no-unused-vars + t.equal(safePrototypeFunction(function() {}).length, 0); + t.equal(safePrototypeFunction(function(a, b) {}).length, 2); // eslint-disable-line no-unused-vars + + t.end(); +}); + +test('FunctionPrototype.name', function(t) { + t.equal(Function.prototype.name, ''); + t.equal(FunctionPrototype.name, ''); + + t.equal((() => {}).name, ''); + t.equal(function() {}.name, ''); + t.equal(function foo() {}.name, 'foo'); + + t.equal(safePrototypeFunction(() => {}).name, ''); + t.equal(safePrototypeFunction(function() {}).name, ''); + t.equal(safePrototypeFunction(function foo() {}).name, 'foo'); + + t.end(); +}); + +test('Object.getPrototypeOf(FunctionPrototype)', function(t) { + t.equal(Object.getPrototypeOf(FunctionPrototype), ObjectPrototype); + t.end(); +}); + +test('FunctionPrototype.constructor', function(t) { + t.equal(FunctionPrototype.constructor, undefined); + t.end(); +}); + +test("FunctionPrototype doesn't leak", function(t) { + assertNoLeak(t, FunctionPrototype); + + const fn = function() {}; + Object.setPrototypeOf(fn, FunctionPrototype); + fn.prototype.__proto__ = FunctionPrototype; + + assertNoLeak(t, fn); + t.end(); +}); + +test("FunctionPrototype functions doesn't leak", function(t) { + const descriptors = Object.getOwnPropertyDescriptors(FunctionPrototype); + for (const [name, descriptor] of Object.entries(descriptors)) { + if (typeof descriptor.value === 'function') { + assertNoLeak(t, descriptor.value, name); + } else if (typeof descriptor.get === 'function') { + assertNoLeak(t, descriptor.get, `${name} getter`); + assertNoLeak(t, descriptor.set, `${name} setter`); + } + } + t.end(); +}); + +test("ObjectPrototype functions doesn't leak", function(t) { + const descriptors = Object.getOwnPropertyDescriptors(ObjectPrototype); + for (const [name, descriptor] of Object.entries(descriptors)) { + if (typeof descriptor.value === 'function') { + assertNoLeak(t, descriptor.value, name); + } else if (typeof descriptor.get === 'function') { + assertNoLeak(t, descriptor.get, `${name} getter`); + assertNoLeak(t, descriptor.set, `${name} setter`); + } + } + t.end(); +}); + +test('ObjectPrototype.__proto__', function(t) { + t.equal(ObjectPrototype.__proto__, null); + t.end(); +}); + +test('Object.getPrototypeOf(ObjectPrototype)', function(t) { + t.equal(Object.getPrototypeOf(ObjectPrototype), null); + t.end(); +}); + +test('ObjectPrototype.constructor', function(t) { + t.equal(ObjectPrototype.constructor, undefined); + t.end(); +}); + +const generators = [create, assign, () => Object.create(ObjectPrototype)]; + +generators.forEach(generator => { + test("functions doesn't leak", function(t) { + const obj = generator(); + for (const name of functions) { + assertNoLeak(t, obj[name], name); + assertNoLeak(t, obj[name].constructor, `${name}.constructor`); + assertNoLeak(t, obj[name].bind, `${name}.bind`); + assertNoLeak(t, obj[name].apply, `${name}.apply`); + assertNoLeak(t, obj[name].call, `${name}.call`); + assertNoLeak(t, obj[name].toString, `${name}.toString`); + } + t.end(); + }); + + test('inheritance', function(t) { + assertNoLeak(t, Object.create(generator())); + t.end(); + }); + + test('Object.getPrototypeOf', function(t) { + const obj = generator(); + t.equal(Object.getPrototypeOf(obj), ObjectPrototype); + t.end(); + }); + + test('__proto__', function(t) { + const obj = generator(); + t.equal(obj.__proto__, ObjectPrototype); + t.end(); + }); + + test('__proto__ =', function(t) { + const obj = generator(); + const someObj = {}; + obj.__proto__ = someObj; + t.equal(obj.__proto__, someObj); + t.equal(Object.getPrototypeOf(obj), someObj); + t.end(); + }); + + test('__proto__.__proto__', function(t) { + const obj = generator(); + t.equal(obj.__proto__.__proto__, null); + t.end(); + }); + + test('constructor', function(t) { + const obj = generator(); + t.equal(obj.constructor, undefined); + t.end(); + }); + + test('hasOwnProperty', function(t) { + const obj1 = generator(); + const obj2 = Object.create(obj1); + + obj1.foo = 42; + + assertExecution(t, 'hasOwnProperty', { obj: obj1, args: ['foo'], expected: true }); + assertExecution(t, 'hasOwnProperty', { obj: obj1, args: ['bar'], expected: false }); + assertExecution(t, 'hasOwnProperty', { obj: obj1, args: ['hasOwnProperty'], expected: false }); + + assertExecution(t, 'hasOwnProperty', { obj: obj2, args: ['foo'], expected: false }); + assertExecution(t, 'hasOwnProperty', { obj: obj2, args: ['hasOwnProperty'], expected: false }); + + assertNonExecutingFunctionPrototypeFunctions(t, 'hasOwnProperty', { obj: obj1 }); + assertNonExecutingFunctionPrototypeFunctions(t, 'hasOwnProperty', { obj: obj2 }); + + t.end(); + }); + + test('isPrototypeOf', function(t) { + const obj1 = generator(); + const obj2 = Object.create(obj1); + + t.equal(Object.prototype.isPrototypeOf(ObjectPrototype), false); + t.equal(Object.prototype.isPrototypeOf(obj1), false); + t.equal(Object.prototype.isPrototypeOf(obj2), false); + + assertExecution(t, 'isPrototypeOf', { obj: ObjectPrototype, args: [obj1], expected: true }); + assertExecution(t, 'isPrototypeOf', { obj: ObjectPrototype, args: [obj2], expected: true }); + + assertExecution(t, 'isPrototypeOf', { obj: obj1, args: [obj2], expected: true }); + assertExecution(t, 'isPrototypeOf', { obj: obj2, args: [obj1], expected: false }); + + assertNonExecutingFunctionPrototypeFunctions(t, 'isPrototypeOf', { obj: obj1 }); + assertNonExecutingFunctionPrototypeFunctions(t, 'isPrototypeOf', { obj: obj2 }); + + t.end(); + }); + + test('propertyIsEnumerable', function(t) { + const obj = generator(); + Object.defineProperty(obj, 'foo', { enumerable: false }); + Object.defineProperty(obj, 'bar', { enumerable: true }); + assertExecution(t, 'propertyIsEnumerable', { obj, args: ['foo'], expected: false }); + assertExecution(t, 'propertyIsEnumerable', { obj, args: ['bar'], expected: true }); + assertExecution(t, 'propertyIsEnumerable', { obj, args: ['invalid'], expected: false }); + assertNonExecutingFunctionPrototypeFunctions(t, 'propertyIsEnumerable', { obj }); + t.end(); + }); + + test('toLocaleString', function(t) { + const obj = generator(); + assertFunction(t, 'toLocaleString', { obj, expected: obj.toString() }); + t.end(); + }); + + test('toString', function(t) { + assertFunction(t, 'toString', { generator, expected: '[object Object]' }); + t.end(); + }); + + test('valueOf', function(t) { + const obj = generator(); + assertFunction(t, 'valueOf', { obj, expected: obj }); + t.end(); + }); + + test('__defineGetter__', function(t) { + t.plan(16); + assertFunction(t, '__defineGetter__', { + generator, + args: ['foo', () => 'works!'], + expected: undefined, + afterCall: obj => { + t.equal(obj.foo, 'works!', 'should return value when accessing getter property'); + }, + }); + }); + + test('__defineSetter__', function(t) { + t.plan(16); + assertFunction(t, '__defineSetter__', { + generator, + args: ['foo', x => t.equal(x, 'works!', 'should call setter with set value')], + expected: undefined, + afterCall: obj => { + obj.foo = 'works!'; + }, + }); + }); + + test('__lookupGetter__', function(t) { + t.plan(11); + + const obj = generator(); + obj.__defineGetter__('foo', () => 'works!'); + + assertFunction(t, '__lookupGetter__', { + obj, + args: ['foo'], + assert: (getter, msg) => t.equal(getter(), 'works!', msg + '()'), + }); + }); + + test('__lookupSetter__', function(t) { + t.plan(11); + + const obj = generator(); + let msg; + + obj.__defineSetter__('foo', x => { + t.equal(x, 'works!', msg); + }); + + assertFunction(t, '__lookupSetter__', { + obj, + args: ['foo'], + assert: (setter, _msg) => { + msg = _msg + '()'; + setter('works!'); + }, + }); + }); +}); + +test('assign', function(t) { + const obj1 = { a: 1, b: 2 }; + const obj2 = { a: 2, c: 3 }; + const obj3 = assign(obj1, obj2); + obj1.b = 42; + obj2.c = 42; + t.equal(obj3.a, 2); + t.equal(obj3.b, 2); + t.equal(obj3.c, 3); + obj3.a = 42; + t.equal(obj1.a, 1); + t.equal(obj2.a, 2); + t.end(); +}); + +function assertFunction(t, name, opts) { + assertExecution(t, name, opts); + assertNonExecutingFunctionPrototypeFunctions(t, name, opts); +} + +function assertNonExecutingFunctionPrototypeFunctions( + t, + name, + { obj: _obj, generator = () => _obj } +) { + const obj = generator(); + + t.throws( + function() { + obj[name].arguments; // eslint-disable-line no-unused-expressions + }, + /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, + `strict: obj.${name}.arguments` + ); + t.throws( + function() { + obj[name].arguments = 42; + }, + /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, + `strict: obj.${name}.arguments =` + ); + + t.throws( + function() { + obj[name].caller; // eslint-disable-line no-unused-expressions + }, + /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, + `strict: obj.${name}.caller` + ); + t.throws( + function() { + obj[name].caller = 42; + }, + /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, + `strict: obj.${name}.caller =` + ); + + t.equal(obj[name].toString(obj), {}[name].toString(), `obj.${name}.toString()`); + + assertNoLeak(t, obj[name].bind(obj), `obj.${name}.bind(obj)`); +} + +function assertExecution( + t, + name, + { + obj: _obj, + generator = () => _obj, + args = [], + expected: _expected, + assert = (result, msg) => t.equal(result, _expected, msg), + afterCall = () => {}, + } +) { + const tests = [ + [obj => obj[name](...args), `obj.${name}(...args)`], + [obj => obj[name].apply(obj, args), `obj.${name}.apply(obj, args)`], + [obj => obj[name].bind(obj)(...args), `obj.${name}.bind(obj)(...args)`], + [obj => obj[name].bind(obj, ...args)(), `obj.${name}.bind(obj, ...args)()`], + [obj => obj[name].call(obj, ...args), `obj.${name}.call(obj, ...args)`], // eslint-disable-line no-useless-call + ]; + + for (const [test, msg] of tests) { + const obj = generator(); + assert(test(obj), msg); + afterCall(obj); + } +} + +function assertNoLeak(t, obj, name = '') { + t.ok(doesObjectLeak(obj) === false, `${name}${name ? ' ' : ''}should not leak Object.prototype`); +} + +function doesObjectLeak(obj, seen = new Set(), indent = '') { + debug(`${indent}obj [root: ${obj === Object.prototype}]:`, obj); + + if (obj === null || obj === undefined) return false; + if (obj === Object.prototype) return true; + + seen.add(obj); + + debug( + `${indent}obj.constructor [set: ${!!obj.constructor}, root: ${obj.constructor === + Object.prototype}, seen: ${seen.has(obj.constructor)}]` + ); + if (obj.constructor && !seen.has(obj.constructor)) { + const result = doesObjectLeak(obj.constructor, seen, indent + ' '); + if (result === true) return true; + } + debug( + `${indent}obj.prototype [set: ${!!obj.prototype}, root: ${obj.prototype === + Object.prototype}, seen: ${seen.has(obj.prototype)}]` + ); + if (obj.prototype && !seen.has(obj.prototype)) { + const result = doesObjectLeak(obj.prototype, seen, indent + ' '); + if (result === true) return true; + } + debug( + `${indent}obj.__proto__ [set: ${!!obj.__proto__}, root: ${obj.__proto__ === + Object.prototype}, seen: ${seen.has(obj.__proto__)}]` + ); + if (obj.__proto__ && !seen.has(obj.__proto__)) { + const result = doesObjectLeak(obj.__proto__, seen, indent + ' '); + if (result === true) return true; + } + + return false; +} + +function isSloppy(fn) { + return Object.prototype.hasOwnProperty.call(fn, 'caller'); +} diff --git a/packages/object-prototype/yarn.lock b/packages/object-prototype/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/object-prototype/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7c7cd958405b5..fbbd71dac7a7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21877,18 +21877,6 @@ object-path@0.11.4: resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= -object-prototype-functions@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object-prototype-functions/-/object-prototype-functions-1.2.0.tgz#59ffa520f2e7ef3b3ba427ba81178eb8feca1048" - integrity sha512-7pfkHi5KHFz/B3RhQVORykDYF2CdQY/h1isJzCJJfEubKkSO59io1raYHAMUrx0Im3fUSrU1oX6E4KeCwyU/jQ== - -object-prototype@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/object-prototype/-/object-prototype-3.0.1.tgz#d6635abf032b216241f7703e96b62d301fdb1aad" - integrity sha512-hKL+jWV9/WIzmSCAHx5jW9yUxeXcujCQmRSkQQbZlKSuFwZ+ZNRuVPChRZmg8TCD/ztLeRJsB7m5HMCTbnXbWA== - dependencies: - object-prototype-functions "^1.2.0" - object-values@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/object-values/-/object-values-1.0.0.tgz#72af839630119e5b98c3b02bb8c27e3237158105" From 1a573dcdbc9d8ac2f6679beb1e6be6d2995507cd Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 3 Mar 2020 19:22:39 +0100 Subject: [PATCH 14/17] test: improve output in case pollution happens --- test/harden/child_process.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/harden/child_process.js b/test/harden/child_process.js index fb7f4b2afcfce..5349a78500f37 100644 --- a/test/harden/child_process.js +++ b/test/harden/child_process.js @@ -23,7 +23,7 @@ const cp = require('child_process'); const path = require('path'); const test = require('tape'); -Object.prototype.POLLUTED = 'yes'; // eslint-disable-line no-extend-native +Object.prototype.POLLUTED = 'polluted!'; // eslint-disable-line no-extend-native const notSet = [null, undefined]; @@ -32,7 +32,7 @@ test.onFinish(() => { }); test('test setup ok', t => { - t.equal({}.POLLUTED, 'yes'); + t.equal({}.POLLUTED, 'polluted!'); t.end(); }); From bccd4c67a64f3a174ca0f2ce723bf8bc86ec8da2 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 3 Mar 2020 19:21:31 +0100 Subject: [PATCH 15/17] refactor: limit scope to only deal with child processes --- .eslintrc.js | 10 - package.json | 1 - .../object-prototype-functions/.npmignore | 1 - packages/object-prototype-functions/README.md | 48 -- packages/object-prototype-functions/index.js | 40 -- .../object-prototype-functions/package.json | 28 -- packages/object-prototype-functions/test.js | 52 -- packages/object-prototype/.npmignore | 1 - packages/object-prototype/README.md | 82 --- packages/object-prototype/index.js | 146 ------ .../object-prototype/lib/wrap_function.js | 41 -- packages/object-prototype/package.json | 33 -- packages/object-prototype/test/functions.js | 23 - packages/object-prototype/test/test.js | 473 ------------------ packages/object-prototype/yarn.lock | 1 - src/setup_node_env/harden.js | 6 - src/setup_node_env/patches/child_process.js | 31 +- test/harden/process_env.js | 42 -- 18 files changed, 10 insertions(+), 1049 deletions(-) delete mode 100644 packages/object-prototype-functions/.npmignore delete mode 100644 packages/object-prototype-functions/README.md delete mode 100644 packages/object-prototype-functions/index.js delete mode 100644 packages/object-prototype-functions/package.json delete mode 100644 packages/object-prototype-functions/test.js delete mode 100644 packages/object-prototype/.npmignore delete mode 100644 packages/object-prototype/README.md delete mode 100644 packages/object-prototype/index.js delete mode 100644 packages/object-prototype/lib/wrap_function.js delete mode 100644 packages/object-prototype/package.json delete mode 100644 packages/object-prototype/test/functions.js delete mode 100644 packages/object-prototype/test/test.js delete mode 120000 packages/object-prototype/yarn.lock delete mode 100644 test/harden/process_env.js diff --git a/.eslintrc.js b/.eslintrc.js index cc07f02d897e2..b730b88a1b9cf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -524,16 +524,6 @@ module.exports = { }, }, - /** - * object-prototype package specific rules - */ - { - files: ['packages/object-prototype/**/*.js'], - rules: { - 'mocha/handle-done-callback': 'off', // TODO: Find a way to disable all mocha rules - }, - }, - /** * APM overrides */ diff --git a/package.json b/package.json index 327c41600f107..7a815856c3d5c 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,6 @@ "ngreact": "0.5.1", "node-fetch": "1.7.3", "node-forge": "^0.9.1", - "object-prototype": "3.0.4", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", diff --git a/packages/object-prototype-functions/.npmignore b/packages/object-prototype-functions/.npmignore deleted file mode 100644 index daa602947e471..0000000000000 --- a/packages/object-prototype-functions/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test.js diff --git a/packages/object-prototype-functions/README.md b/packages/object-prototype-functions/README.md deleted file mode 100644 index e0cca078a185b..0000000000000 --- a/packages/object-prototype-functions/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# object-prototype-functions - -An array containing the names of the functions on `Object.prototype`. - -[![npm](https://img.shields.io/npm/v/object-prototype-functions.svg)](https://www.npmjs.com/package/object-prototype-functions) - -## Installation - -``` -npm install object-prototype-functions --save -``` - -## Usage - -```js -const ObjectPrototypeFunctions = require('object-prototype-functions') - -console.log('The functions of Object.prototype are:') -console.log(ObjectPrototypeFunctions.join(', ')) // hasOwnProperty, isPrototypeOf... -``` - -## API - -### `ObjectPrototypeFunctions` - -An array containing the names of the functions on `Object.prototype`. - -### `ObjectPrototypeFunctions.deprecated` - -An array containing the names of the deprecated functions on -`Object.prototype`. - -### `ObjectPrototypeFunctions.nonSpec` - -An array containing the names of the non-spec'd functions on -`Object.prototype`. - -### `ObjectPrototypeFunctions.nodejs` - -An array containing the all names of the functions on `Object.prototype` -that are normally available in Node.js - both the spec'd and deprecated -functions. - -### `ObjectPrototypeFunctions.all` - -An array containing the all names of the functions on -`Object.prototype`, both the spec'd, deprecated, and non-spec'd -functions. diff --git a/packages/object-prototype-functions/index.js b/packages/object-prototype-functions/index.js deleted file mode 100644 index 07e6ee1f8fc69..0000000000000 --- a/packages/object-prototype-functions/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = [ - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'toLocaleString', - 'toString', - 'valueOf', -]; - -module.exports.deprecated = [ - '__defineGetter__', - '__defineSetter__', - '__lookupGetter__', - '__lookupSetter__', -]; - -module.exports.nonSpec = ['eval', 'unwatch', 'watch']; - -module.exports.nodejs = module.exports.concat(module.exports.deprecated); - -module.exports.all = module.exports.concat(module.exports.deprecated, module.exports.nonSpec); diff --git a/packages/object-prototype-functions/package.json b/packages/object-prototype-functions/package.json deleted file mode 100644 index e2b423d4225de..0000000000000 --- a/packages/object-prototype-functions/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "object-prototype-functions", - "version": "1.2.0", - "description": "An array containing the names of the functions on `Object.prototype`", - "main": "index.js", - "dependencies": {}, - "devDependencies": {}, - "scripts": { - "test": "node test.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/elastic/kibana.git" - }, - "keywords": [ - "object", - "prototype", - "function", - "functions", - "name" - ], - "author": "Thomas Watson (https://twitter.com/wa7son)", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/elastic/kibana/issues" - }, - "homepage": "https://github.com/elastic/kibana/blob/master/packages/object-prototype-functions/README.md" -} diff --git a/packages/object-prototype-functions/test.js b/packages/object-prototype-functions/test.js deleted file mode 100644 index c6483a50a9f8f..0000000000000 --- a/packages/object-prototype-functions/test.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const assert = require('assert'); -const ObjectPrototypeFunctions = require('./'); - -assert(Array.isArray(ObjectPrototypeFunctions)); -assert(ObjectPrototypeFunctions.length > 0); - -ObjectPrototypeFunctions.forEach(name => { - assert.strictEqual(typeof Object.prototype[name], 'function'); -}); - -assert(Array.isArray(ObjectPrototypeFunctions.deprecated)); -assert(ObjectPrototypeFunctions.deprecated.length > 0); - -ObjectPrototypeFunctions.deprecated.forEach(name => { - assert.strictEqual(typeof Object.prototype[name], 'function'); -}); - -assert(Array.isArray(ObjectPrototypeFunctions.nonSpec)); -assert(ObjectPrototypeFunctions.nonSpec.length > 0); - -assert(Array.isArray(ObjectPrototypeFunctions.nodejs)); -assert.strictEqual( - ObjectPrototypeFunctions.nodejs.length, - ObjectPrototypeFunctions.length + ObjectPrototypeFunctions.deprecated.length -); - -assert(Array.isArray(ObjectPrototypeFunctions.all)); -assert.strictEqual( - ObjectPrototypeFunctions.all.length, - ObjectPrototypeFunctions.length + - ObjectPrototypeFunctions.deprecated.length + - ObjectPrototypeFunctions.nonSpec.length -); diff --git a/packages/object-prototype/.npmignore b/packages/object-prototype/.npmignore deleted file mode 100644 index 9daeafb9864cf..0000000000000 --- a/packages/object-prototype/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/packages/object-prototype/README.md b/packages/object-prototype/README.md deleted file mode 100644 index 2a95086667537..0000000000000 --- a/packages/object-prototype/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# object-prototype - -A replacement prototype for `Object.prototype` with all the same -functions. - -[![npm](https://img.shields.io/npm/v/object-prototype.svg)](https://www.npmjs.com/package/object-prototype) - -## Installation - -``` -npm install object-prototype --save -``` - -## Usage - -```js -const { create, ObjectPrototype } = require('object-prototype'); - -const obj1 = create(); -const obj2 = {}; - -console.log(Object.prototype.isPrototypeOf(obj1)); // false -console.log(ObjectPrototype.isPrototypeOf(obj1)); // true - -console.log(Object.prototype.isPrototypeOf(obj2)); // true -console.log(ObjectPrototype.isPrototypeOf(obj2)); // false - -Object.prototype.foo = 42; - -console.log(obj1.foo); // undefined -console.log(obj2.foo); // 42 -``` - -## API - -### `ObjectPrototype` - -The `ObjectPrototype` property exposed by this module is ment as a -replacement to `Object.prototype` and has the following ECMAScript -spec'd functions: - -- [`ObjectPrototype.hasOwnProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) -- [`ObjectPrototype.isPrototypeOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf) -- [`ObjectPrototype.propertyIsEnumerable()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable) -- [`ObjectPrototype.toLocaleString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toLocaleString) -- [`ObjectPrototype.toString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) -- [`ObjectPrototype.valueOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf) - -And the following functions which are considered deprecated according to -the ECMAScript specification: - -- [`ObjectPrototype.__defineGetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__) -- [`ObjectPrototype.__defineSetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineSetter__) -- [`ObjectPrototype.__lookupGetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__lookupGetter__) -- [`ObjectPrototype.__lookupSetter__()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__lookupSetter__) - -### `object = create()` - -The `create` function is a convenience function that returns a new -object with `ObjectPrototype` as its prototype. - -This is equivalent to writing `Object.create(ObjectPrototype)`. - -### `object = assign([...objects])` - -The `assign` function is a convenience function that returns a new -object with all the same properties as the provided objects, but with -`ObjectPrototype` as its prototype. - -This is equivalent to writing -`Object.assign(Object.create(ObjectPrototype), ...objects)`. - -### `FunctionPrototype` - -The `FunctionPrototype` property exposed by this module is ment as a -replacement to `Function.prototype` and exposes the same properties. - -### `function = safePrototypeFunction(function)` - -Given a function as the first argument, `safePrototypeFunction` will -return another function which wraps the given function in a way so that -it doesn't leak `Object.prototype`. diff --git a/packages/object-prototype/index.js b/packages/object-prototype/index.js deleted file mode 100644 index ac7cc93716f11..0000000000000 --- a/packages/object-prototype/index.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const functions = require('object-prototype-functions').nodejs; -const wrap = require('./lib/wrap_function'); - -const ObjectPrototype = Object.create(null); -const FunctionPrototype = Object.create(ObjectPrototype); - -exports.ObjectPrototype = ObjectPrototype; -exports.create = () => Object.create(ObjectPrototype); -exports.assign = (...args) => Object.assign(exports.create(), ...args); - -exports.FunctionPrototype = FunctionPrototype; -exports.safePrototypeFunction = safePrototypeFunction; - -/** - * ObjectPrototype - */ - -const descriptors = { - ['__proto__']: { - get: safePrototypeFunction(function() { - 'use strict'; // eslint-disable-line strict - return Object.getPrototypeOf(this); - }), - set: safePrototypeFunction(function(proto) { - 'use strict'; // eslint-disable-line strict - Object.setPrototypeOf(this, proto); - }), - enumerable: false, - configurable: true, - }, - constructor: { - value: undefined, // NOTE: This doesn't try to mimic the usual Object behavior - writable: true, - enumerable: false, - configurable: true, - }, -}; - -for (const name of functions) { - const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, name); - descriptor.value = safePrototypeFunction(descriptor.value); - descriptors[name] = descriptor; -} - -Object.defineProperties(ObjectPrototype, descriptors); - -/** - * FunctionPrototype - */ - -Object.defineProperties(FunctionPrototype, { - length: { value: 0, writable: false, enumerable: false, configurable: true }, - name: { value: '', writable: false, enumerable: false, configurable: true }, - arguments: { - get: safePrototypeFunction(Function.prototype.__lookupGetter__('arguments')), - set: safePrototypeFunction(Function.prototype.__lookupSetter__('arguments')), - enumerable: false, - configurable: true, - }, - caller: { - get: safePrototypeFunction(Function.prototype.__lookupGetter__('caller')), - set: safePrototypeFunction(Function.prototype.__lookupSetter__('caller')), - enumerable: false, - configurable: true, - }, - constructor: { - value: undefined, // NOTE: This doesn't try to mimic the usual Object behavior - writable: true, - enumerable: false, - configurable: true, - }, - apply: { - value: safePrototypeFunction(Function.prototype.apply), - writable: true, - enumerable: false, - configurable: true, - }, - bind: { - value: safePrototypeFunction(function bind() { - 'use strict'; // eslint-disable-line strict - const bound = Function.prototype.bind.apply(this, arguments); - Object.setPrototypeOf(bound, FunctionPrototype); - return bound; - }), - writable: true, - enumerable: false, - configurable: true, - }, - call: { - value: safePrototypeFunction(Function.prototype.call), - writable: true, - enumerable: false, - configurable: true, - }, - toString: { - value: safePrototypeFunction(function toString() { - 'use strict'; // eslint-disable-line strict - // TODO: Should we really do this? - return `function ${this.name}() { [native code] }`; - // Alternatively we expose the normal behavior: - // return Function.prototype.toString.apply(this, arguments) - }), - writable: true, - enumerable: false, - configurable: true, - }, - - // NOTE: I skipped overriding Function.prototype[Symbol.hasInstance] as - // this function would only be called if `FunctionPrototype` is on the - // right hand side of the `instaceof` operator. -}); - -function safePrototypeFunction(original, name) { - const wrapped = wrap(name || original.name, original); - - // In strict mode, the expected descriptors are: length, name, prototype - // In non-strict mode, the expected descriptors are: length, name, arguments, caller, prototype - const descriptors = Object.getOwnPropertyDescriptors(original); - delete descriptors.name; // will be set automatally when the function is created below - delete descriptors.prototype; // we'll use the `wrapped` functions own `prototype` property - - Object.defineProperties(wrapped, descriptors); - Object.setPrototypeOf(wrapped, FunctionPrototype); - Object.setPrototypeOf(wrapped.prototype, ObjectPrototype); - - return wrapped; -} diff --git a/packages/object-prototype/lib/wrap_function.js b/packages/object-prototype/lib/wrap_function.js deleted file mode 100644 index 8c3ff66514808..0000000000000 --- a/packages/object-prototype/lib/wrap_function.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = function(name, fn) { - if (isSloppy(fn)) { - // Hack to give the function the same name as the original - return { - [name]: function() { - return fn.apply(this, arguments); - }, - }[name]; - } else { - // Hack to give the function the same name as the original - return { - [name]: function() { - 'use strict'; // eslint-disable-line strict - return fn.apply(this, arguments); - }, - }[name]; - } -}; - -function isSloppy(fn) { - return Object.prototype.hasOwnProperty.call(fn, 'caller'); -} diff --git a/packages/object-prototype/package.json b/packages/object-prototype/package.json deleted file mode 100644 index 547b8c146cdcf..0000000000000 --- a/packages/object-prototype/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "object-prototype", - "version": "3.0.4", - "description": "A replacement prototype for `Object.prototype` with all the same functions", - "main": "index.js", - "dependencies": { - "object-prototype-functions": "1.2.0" - }, - "devDependencies": { - "debug": "^4.1.1", - "tape": "^4.13.0" - }, - "scripts": { - "test": "node test/test.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/elastic/kibana.git" - }, - "keywords": [ - "object", - "prototype", - "pollution", - "polyfill", - "security" - ], - "author": "Thomas Watson (https://twitter.com/wa7son)", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/elastic/kibana/issues" - }, - "homepage": "https://github.com/elastic/kibana/blob/master/packages/object-prototype/README.md" -} diff --git a/packages/object-prototype/test/functions.js b/packages/object-prototype/test/functions.js deleted file mode 100644 index 385224455f8e7..0000000000000 --- a/packages/object-prototype/test/functions.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -exports.sloppy = function() {}; -exports.strict = function() { - 'use strict'; // eslint-disable-line strict -}; diff --git a/packages/object-prototype/test/test.js b/packages/object-prototype/test/test.js deleted file mode 100644 index 6f961c1745eac..0000000000000 --- a/packages/object-prototype/test/test.js +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* eslint-disable no-prototype-builtins, no-proto */ - -const test = require('tape'); -const debug = require('debug')('tests'); -const functions = require('object-prototype-functions').nodejs; -const { sloppy, strict } = require('./functions'); -const { - create, - assign, - ObjectPrototype, - FunctionPrototype, - safePrototypeFunction, -} = require('../'); - -test('isSloppy', function(t) { - t.equal(isSloppy(sloppy), true); - t.equal(isSloppy(strict), false); - t.end(); -}); - -test('doesObjectLeak', function(t) { - t.ok(doesObjectLeak(function() {})); - t.ok(doesObjectLeak(() => {})); - t.ok(doesObjectLeak({})); - t.ok(doesObjectLeak(Object)); - t.ok(doesObjectLeak(Function)); - t.ok(doesObjectLeak(Object.prototype)); - t.ok(doesObjectLeak(Function.prototype)); - t.notOk(doesObjectLeak(Object.create(null))); - t.end(); -}); - -test('safePrototypeFunction maintains strictness', function(t) { - t.equal(isSloppy(safePrototypeFunction(sloppy)), true); - t.equal(isSloppy(safePrototypeFunction(strict)), false); - t.end(); -}); - -test("safePrototypeFunction doesn't leak", function(t) { - const fn = safePrototypeFunction(function() {}); - assertNoLeak(t, fn); - t.end(); -}); - -test('FunctionPrototype.__proto__', function(t) { - t.equal(FunctionPrototype.__proto__, ObjectPrototype); - t.end(); -}); - -test('FunctionPrototype.length', function(t) { - t.equal(Function.prototype.length, 0); - t.equal(FunctionPrototype.length, 0); - - t.equal((() => {}).length, 0); - t.equal(((a, b) => {}).length, 2); // eslint-disable-line no-unused-vars - t.equal(function() {}.length, 0); - t.equal(function(a, b) {}.length, 2); // eslint-disable-line no-unused-vars - - t.equal(safePrototypeFunction(() => {}).length, 0); - t.equal(safePrototypeFunction((a, b) => {}).length, 2); // eslint-disable-line no-unused-vars - t.equal(safePrototypeFunction(function() {}).length, 0); - t.equal(safePrototypeFunction(function(a, b) {}).length, 2); // eslint-disable-line no-unused-vars - - t.end(); -}); - -test('FunctionPrototype.name', function(t) { - t.equal(Function.prototype.name, ''); - t.equal(FunctionPrototype.name, ''); - - t.equal((() => {}).name, ''); - t.equal(function() {}.name, ''); - t.equal(function foo() {}.name, 'foo'); - - t.equal(safePrototypeFunction(() => {}).name, ''); - t.equal(safePrototypeFunction(function() {}).name, ''); - t.equal(safePrototypeFunction(function foo() {}).name, 'foo'); - - t.end(); -}); - -test('Object.getPrototypeOf(FunctionPrototype)', function(t) { - t.equal(Object.getPrototypeOf(FunctionPrototype), ObjectPrototype); - t.end(); -}); - -test('FunctionPrototype.constructor', function(t) { - t.equal(FunctionPrototype.constructor, undefined); - t.end(); -}); - -test("FunctionPrototype doesn't leak", function(t) { - assertNoLeak(t, FunctionPrototype); - - const fn = function() {}; - Object.setPrototypeOf(fn, FunctionPrototype); - fn.prototype.__proto__ = FunctionPrototype; - - assertNoLeak(t, fn); - t.end(); -}); - -test("FunctionPrototype functions doesn't leak", function(t) { - const descriptors = Object.getOwnPropertyDescriptors(FunctionPrototype); - for (const [name, descriptor] of Object.entries(descriptors)) { - if (typeof descriptor.value === 'function') { - assertNoLeak(t, descriptor.value, name); - } else if (typeof descriptor.get === 'function') { - assertNoLeak(t, descriptor.get, `${name} getter`); - assertNoLeak(t, descriptor.set, `${name} setter`); - } - } - t.end(); -}); - -test("ObjectPrototype functions doesn't leak", function(t) { - const descriptors = Object.getOwnPropertyDescriptors(ObjectPrototype); - for (const [name, descriptor] of Object.entries(descriptors)) { - if (typeof descriptor.value === 'function') { - assertNoLeak(t, descriptor.value, name); - } else if (typeof descriptor.get === 'function') { - assertNoLeak(t, descriptor.get, `${name} getter`); - assertNoLeak(t, descriptor.set, `${name} setter`); - } - } - t.end(); -}); - -test('ObjectPrototype.__proto__', function(t) { - t.equal(ObjectPrototype.__proto__, null); - t.end(); -}); - -test('Object.getPrototypeOf(ObjectPrototype)', function(t) { - t.equal(Object.getPrototypeOf(ObjectPrototype), null); - t.end(); -}); - -test('ObjectPrototype.constructor', function(t) { - t.equal(ObjectPrototype.constructor, undefined); - t.end(); -}); - -const generators = [create, assign, () => Object.create(ObjectPrototype)]; - -generators.forEach(generator => { - test("functions doesn't leak", function(t) { - const obj = generator(); - for (const name of functions) { - assertNoLeak(t, obj[name], name); - assertNoLeak(t, obj[name].constructor, `${name}.constructor`); - assertNoLeak(t, obj[name].bind, `${name}.bind`); - assertNoLeak(t, obj[name].apply, `${name}.apply`); - assertNoLeak(t, obj[name].call, `${name}.call`); - assertNoLeak(t, obj[name].toString, `${name}.toString`); - } - t.end(); - }); - - test('inheritance', function(t) { - assertNoLeak(t, Object.create(generator())); - t.end(); - }); - - test('Object.getPrototypeOf', function(t) { - const obj = generator(); - t.equal(Object.getPrototypeOf(obj), ObjectPrototype); - t.end(); - }); - - test('__proto__', function(t) { - const obj = generator(); - t.equal(obj.__proto__, ObjectPrototype); - t.end(); - }); - - test('__proto__ =', function(t) { - const obj = generator(); - const someObj = {}; - obj.__proto__ = someObj; - t.equal(obj.__proto__, someObj); - t.equal(Object.getPrototypeOf(obj), someObj); - t.end(); - }); - - test('__proto__.__proto__', function(t) { - const obj = generator(); - t.equal(obj.__proto__.__proto__, null); - t.end(); - }); - - test('constructor', function(t) { - const obj = generator(); - t.equal(obj.constructor, undefined); - t.end(); - }); - - test('hasOwnProperty', function(t) { - const obj1 = generator(); - const obj2 = Object.create(obj1); - - obj1.foo = 42; - - assertExecution(t, 'hasOwnProperty', { obj: obj1, args: ['foo'], expected: true }); - assertExecution(t, 'hasOwnProperty', { obj: obj1, args: ['bar'], expected: false }); - assertExecution(t, 'hasOwnProperty', { obj: obj1, args: ['hasOwnProperty'], expected: false }); - - assertExecution(t, 'hasOwnProperty', { obj: obj2, args: ['foo'], expected: false }); - assertExecution(t, 'hasOwnProperty', { obj: obj2, args: ['hasOwnProperty'], expected: false }); - - assertNonExecutingFunctionPrototypeFunctions(t, 'hasOwnProperty', { obj: obj1 }); - assertNonExecutingFunctionPrototypeFunctions(t, 'hasOwnProperty', { obj: obj2 }); - - t.end(); - }); - - test('isPrototypeOf', function(t) { - const obj1 = generator(); - const obj2 = Object.create(obj1); - - t.equal(Object.prototype.isPrototypeOf(ObjectPrototype), false); - t.equal(Object.prototype.isPrototypeOf(obj1), false); - t.equal(Object.prototype.isPrototypeOf(obj2), false); - - assertExecution(t, 'isPrototypeOf', { obj: ObjectPrototype, args: [obj1], expected: true }); - assertExecution(t, 'isPrototypeOf', { obj: ObjectPrototype, args: [obj2], expected: true }); - - assertExecution(t, 'isPrototypeOf', { obj: obj1, args: [obj2], expected: true }); - assertExecution(t, 'isPrototypeOf', { obj: obj2, args: [obj1], expected: false }); - - assertNonExecutingFunctionPrototypeFunctions(t, 'isPrototypeOf', { obj: obj1 }); - assertNonExecutingFunctionPrototypeFunctions(t, 'isPrototypeOf', { obj: obj2 }); - - t.end(); - }); - - test('propertyIsEnumerable', function(t) { - const obj = generator(); - Object.defineProperty(obj, 'foo', { enumerable: false }); - Object.defineProperty(obj, 'bar', { enumerable: true }); - assertExecution(t, 'propertyIsEnumerable', { obj, args: ['foo'], expected: false }); - assertExecution(t, 'propertyIsEnumerable', { obj, args: ['bar'], expected: true }); - assertExecution(t, 'propertyIsEnumerable', { obj, args: ['invalid'], expected: false }); - assertNonExecutingFunctionPrototypeFunctions(t, 'propertyIsEnumerable', { obj }); - t.end(); - }); - - test('toLocaleString', function(t) { - const obj = generator(); - assertFunction(t, 'toLocaleString', { obj, expected: obj.toString() }); - t.end(); - }); - - test('toString', function(t) { - assertFunction(t, 'toString', { generator, expected: '[object Object]' }); - t.end(); - }); - - test('valueOf', function(t) { - const obj = generator(); - assertFunction(t, 'valueOf', { obj, expected: obj }); - t.end(); - }); - - test('__defineGetter__', function(t) { - t.plan(16); - assertFunction(t, '__defineGetter__', { - generator, - args: ['foo', () => 'works!'], - expected: undefined, - afterCall: obj => { - t.equal(obj.foo, 'works!', 'should return value when accessing getter property'); - }, - }); - }); - - test('__defineSetter__', function(t) { - t.plan(16); - assertFunction(t, '__defineSetter__', { - generator, - args: ['foo', x => t.equal(x, 'works!', 'should call setter with set value')], - expected: undefined, - afterCall: obj => { - obj.foo = 'works!'; - }, - }); - }); - - test('__lookupGetter__', function(t) { - t.plan(11); - - const obj = generator(); - obj.__defineGetter__('foo', () => 'works!'); - - assertFunction(t, '__lookupGetter__', { - obj, - args: ['foo'], - assert: (getter, msg) => t.equal(getter(), 'works!', msg + '()'), - }); - }); - - test('__lookupSetter__', function(t) { - t.plan(11); - - const obj = generator(); - let msg; - - obj.__defineSetter__('foo', x => { - t.equal(x, 'works!', msg); - }); - - assertFunction(t, '__lookupSetter__', { - obj, - args: ['foo'], - assert: (setter, _msg) => { - msg = _msg + '()'; - setter('works!'); - }, - }); - }); -}); - -test('assign', function(t) { - const obj1 = { a: 1, b: 2 }; - const obj2 = { a: 2, c: 3 }; - const obj3 = assign(obj1, obj2); - obj1.b = 42; - obj2.c = 42; - t.equal(obj3.a, 2); - t.equal(obj3.b, 2); - t.equal(obj3.c, 3); - obj3.a = 42; - t.equal(obj1.a, 1); - t.equal(obj2.a, 2); - t.end(); -}); - -function assertFunction(t, name, opts) { - assertExecution(t, name, opts); - assertNonExecutingFunctionPrototypeFunctions(t, name, opts); -} - -function assertNonExecutingFunctionPrototypeFunctions( - t, - name, - { obj: _obj, generator = () => _obj } -) { - const obj = generator(); - - t.throws( - function() { - obj[name].arguments; // eslint-disable-line no-unused-expressions - }, - /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, - `strict: obj.${name}.arguments` - ); - t.throws( - function() { - obj[name].arguments = 42; - }, - /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, - `strict: obj.${name}.arguments =` - ); - - t.throws( - function() { - obj[name].caller; // eslint-disable-line no-unused-expressions - }, - /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, - `strict: obj.${name}.caller` - ); - t.throws( - function() { - obj[name].caller = 42; - }, - /'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them/, - `strict: obj.${name}.caller =` - ); - - t.equal(obj[name].toString(obj), {}[name].toString(), `obj.${name}.toString()`); - - assertNoLeak(t, obj[name].bind(obj), `obj.${name}.bind(obj)`); -} - -function assertExecution( - t, - name, - { - obj: _obj, - generator = () => _obj, - args = [], - expected: _expected, - assert = (result, msg) => t.equal(result, _expected, msg), - afterCall = () => {}, - } -) { - const tests = [ - [obj => obj[name](...args), `obj.${name}(...args)`], - [obj => obj[name].apply(obj, args), `obj.${name}.apply(obj, args)`], - [obj => obj[name].bind(obj)(...args), `obj.${name}.bind(obj)(...args)`], - [obj => obj[name].bind(obj, ...args)(), `obj.${name}.bind(obj, ...args)()`], - [obj => obj[name].call(obj, ...args), `obj.${name}.call(obj, ...args)`], // eslint-disable-line no-useless-call - ]; - - for (const [test, msg] of tests) { - const obj = generator(); - assert(test(obj), msg); - afterCall(obj); - } -} - -function assertNoLeak(t, obj, name = '') { - t.ok(doesObjectLeak(obj) === false, `${name}${name ? ' ' : ''}should not leak Object.prototype`); -} - -function doesObjectLeak(obj, seen = new Set(), indent = '') { - debug(`${indent}obj [root: ${obj === Object.prototype}]:`, obj); - - if (obj === null || obj === undefined) return false; - if (obj === Object.prototype) return true; - - seen.add(obj); - - debug( - `${indent}obj.constructor [set: ${!!obj.constructor}, root: ${obj.constructor === - Object.prototype}, seen: ${seen.has(obj.constructor)}]` - ); - if (obj.constructor && !seen.has(obj.constructor)) { - const result = doesObjectLeak(obj.constructor, seen, indent + ' '); - if (result === true) return true; - } - debug( - `${indent}obj.prototype [set: ${!!obj.prototype}, root: ${obj.prototype === - Object.prototype}, seen: ${seen.has(obj.prototype)}]` - ); - if (obj.prototype && !seen.has(obj.prototype)) { - const result = doesObjectLeak(obj.prototype, seen, indent + ' '); - if (result === true) return true; - } - debug( - `${indent}obj.__proto__ [set: ${!!obj.__proto__}, root: ${obj.__proto__ === - Object.prototype}, seen: ${seen.has(obj.__proto__)}]` - ); - if (obj.__proto__ && !seen.has(obj.__proto__)) { - const result = doesObjectLeak(obj.__proto__, seen, indent + ' '); - if (result === true) return true; - } - - return false; -} - -function isSloppy(fn) { - return Object.prototype.hasOwnProperty.call(fn, 'caller'); -} diff --git a/packages/object-prototype/yarn.lock b/packages/object-prototype/yarn.lock deleted file mode 120000 index 3f82ebc9cdbae..0000000000000 --- a/packages/object-prototype/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/src/setup_node_env/harden.js b/src/setup_node_env/harden.js index 4027407310db9..466cbfa0d92cf 100644 --- a/src/setup_node_env/harden.js +++ b/src/setup_node_env/harden.js @@ -18,12 +18,6 @@ */ var hook = require('require-in-the-middle'); -var create = require('object-prototype').create; - -// Ensure `process.env` doesn't inherit from `Object.prototype`. This gives -// partial protection against similar RCE vulnerabilities as described in -// CVE-2019-7609 -process.env = Object.assign(create(), process.env); hook(['child_process'], function(exports, name) { return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js index d1ce5150edf75..e3b85d5337f19 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/patches/child_process.js @@ -17,11 +17,6 @@ * under the License. */ -var op = require('object-prototype'); - -var create = op.create; -var assign = op.assign; - // Ensure, when spawning a new child process, that the `options` and the // `options.env` object passed to the child process function doesn't inherit // from `Object.prototype`. This protects against similar RCE vulnerabilities @@ -48,25 +43,25 @@ function patchOptions(hasArgs) { var pos = 1; if (pos === args.length) { // fn(arg1) - args[pos] = create(); + args[pos] = prototypelessSpawnOpts(); } else if (pos < args.length) { if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) { // fn(arg1, args, ...) pos++; } - if (pos < args.length && typeof args[pos] === 'object' && args[pos] !== null) { + if (typeof args[pos] === 'object' && args[pos] !== null) { // fn(arg1, {}, ...) // fn(arg1, args, {}, ...) args[pos] = prototypelessSpawnOpts(args[pos]); - } else if (pos < args.length && args[pos] == null) { - // fn(arg1, null, ...) - // fn(arg1, args, null, ...) - args[pos] = create(); - } else if (pos < args.length && typeof args[pos] === 'function') { + } else if (args[pos] == null) { + // fn(arg1, null/undefined, ...) + // fn(arg1, args, null/undefined, ...) + args[pos] = prototypelessSpawnOpts(); + } else if (typeof args[pos] === 'function') { // fn(arg1, callback) // fn(arg1, args, callback) - args.splice(pos, 0, create()); + args.splice(pos, 0, prototypelessSpawnOpts()); } } @@ -75,13 +70,7 @@ function patchOptions(hasArgs) { } function prototypelessSpawnOpts(obj) { - var prototypelessObj = assign(obj); - - // The `process.env` fallback has been hardened elsewhere, so here we only - // care about the case where an `env` option is provided. - if (prototypelessObj.env) { - prototypelessObj.env = assign(prototypelessObj.env); - } - + var prototypelessObj = Object.assign(Object.create(null), obj); + prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env || process.env); return prototypelessObj; } diff --git a/test/harden/process_env.js b/test/harden/process_env.js deleted file mode 100644 index bdde7d2f00622..0000000000000 --- a/test/harden/process_env.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -require('../../src/setup_node_env'); - -const test = require('tape'); -const { ObjectPrototype } = require('object-prototype'); - -test('process.env prototype', t => { - t.equal(Object.prototype.isPrototypeOf(process.env), false); - t.equal(ObjectPrototype.isPrototypeOf(process.env), true); - t.end(); -}); - -test('prototype pollution', t => { - Object.prototype.foo = 42; // eslint-disable-line no-extend-native - t.equal(process.env.foo, undefined); - delete Object.prototype.foo; - t.end(); -}); - -test('Object.prototype functions', t => { - t.equal(typeof process.env.hasOwnProperty, 'function'); - t.equal(process.env.hasOwnProperty('HOME'), true); - t.end(); -}); From 7ec6fe369a5c8fa0f3636c0ea47cb1c685147653 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 9 Mar 2020 08:28:57 +0100 Subject: [PATCH 16/17] address review comments --- scripts/test_hardening.js | 25 ++++++++----------------- test/harden/child_process.js | 10 +++++----- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/scripts/test_hardening.js b/scripts/test_hardening.js index 03fdb596f0ab7..c0a20a9ff6cb4 100644 --- a/scripts/test_hardening.js +++ b/scripts/test_hardening.js @@ -18,9 +18,8 @@ */ var execFileSync = require('child_process').execFileSync; -var readdirSync = require('fs').readdirSync; var path = require('path'); -var glob = require('glob'); +var syncGlob = require('glob').sync; var program = require('commander'); program @@ -29,22 +28,14 @@ program .description( 'Run the tests in test/harden directory. If no files are provided, all files within the directory will be run.' ) - .action(function(files) { - if (files.length === 0) files = allFiles(); - files.forEach(function(file) { - var files = glob.sync(file); - files.forEach(function(file) { - if (path.basename(file)[0] === '_') return; - console.log(process.argv[0], file); - execFileSync(process.argv[0], [file], { stdio: 'inherit' }); + .action(function(globs) { + if (globs.length === 0) globs.push(path.join('test', 'harden', '*')); + globs.forEach(function(glob) { + syncGlob(glob).forEach(function(filename) { + if (path.basename(filename)[0] === '_') return; + console.log(process.argv[0], filename); + execFileSync(process.argv[0], [filename], { stdio: 'inherit' }); }); }); }) .parse(process.argv); - -function allFiles() { - var dir = path.join('test', 'harden'); - return readdirSync(dir).map(function(file) { - return path.join(dir, file); - }); -} diff --git a/test/harden/child_process.js b/test/harden/child_process.js index 5349a78500f37..11e2eeb07e0b6 100644 --- a/test/harden/child_process.js +++ b/test/harden/child_process.js @@ -38,7 +38,7 @@ test('test setup ok', t => { // TODO: fork() has been omitted as it doesn't validate its arguments in // Node.js 10 and will throw an internal error asynchronously. This is fixed in -// newer versions. +// newer versions. See https://github.com/elastic/kibana/issues/59628 const functions = ['exec', 'execFile', 'spawn', 'execFileSync', 'execSync', 'spawnSync']; for (const name of functions) { test(`${name}()`, t => { @@ -222,12 +222,12 @@ for (const name of functions) { { const modulePath = path.join('test', 'harden', '_fork.js'); - // TODO: Forked processes don't have any stdout we can monitor without providing options + // NOTE: Forked processes don't have any stdout we can monitor without providing options test.skip('fork(modulePath)', t => { assertProcess(t, cp.fork(modulePath)); }); - // TODO: Forked processes don't have any stdout we can monitor without providing options + // NOTE: Forked processes don't have any stdout we can monitor without providing options test.skip('execFile(file, args)', t => { assertProcess(t, cp.fork(modulePath, [])); }); @@ -273,12 +273,12 @@ for (const name of functions) { }); for (const unset of notSet) { - // TODO: Forked processes don't have any stdout we can monitor without providing options + // NOTE: Forked processes don't have any stdout we can monitor without providing options test.skip(`fork(modulePath, ${unset})`, t => { assertProcess(t, cp.fork(modulePath, unset)); }); - // TODO: Forked processes don't have any stdout we can monitor without providing options + // NOTE: Forked processes don't have any stdout we can monitor without providing options test.skip(`fork(modulePath, ${unset}, ${unset})`, t => { assertProcess(t, cp.fork(modulePath, unset, unset)); }); From 504213211f0e96243e7c58c278d04bb5493f81bc Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 9 Mar 2020 18:14:10 +0100 Subject: [PATCH 17/17] Update src/setup_node_env/patches/child_process.js Co-Authored-By: Larry Gregory --- src/setup_node_env/patches/child_process.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup_node_env/patches/child_process.js b/src/setup_node_env/patches/child_process.js index e3b85d5337f19..b89190d8085e6 100644 --- a/src/setup_node_env/patches/child_process.js +++ b/src/setup_node_env/patches/child_process.js @@ -24,7 +24,7 @@ module.exports = function(cp) { // The `exec` function is currently just a wrapper around `execFile`. So for // now there's no need to patch it. If this changes in the future, our tests - // will fail and we can comment out the line below. + // will fail and we can uncomment the line below. // // cp.exec = new Proxy(cp.exec, { apply: patchOptions() });