From 75320b982db1da7b25bbdfe92791e7263c557406 Mon Sep 17 00:00:00 2001 From: kobelb Date: Thu, 2 Jul 2020 10:15:28 -0700 Subject: [PATCH] Applying changes from https://github.com/elastic/kibana/pull/64985 --- src/setup_node_env/harden.js | 8 ++ src/setup_node_env/patches/lodash.js | 39 ++++++++ src/setup_node_env/patches/lodash_fp.js | 33 +++++++ test/harden/lodash.js | 103 +++++++++++++++++++++ test/harden/lodash_fp.js | 114 ++++++++++++++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 src/setup_node_env/patches/lodash.js create mode 100644 src/setup_node_env/patches/lodash_fp.js create mode 100644 test/harden/lodash.js create mode 100644 test/harden/lodash_fp.js diff --git a/src/setup_node_env/harden.js b/src/setup_node_env/harden.js index dead3db1d60b4d..fc9a65c0e23b3a 100644 --- a/src/setup_node_env/harden.js +++ b/src/setup_node_env/harden.js @@ -22,3 +22,11 @@ var hook = require('require-in-the-middle'); hook(['child_process'], function (exports, name) { return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require }); + +hook(['lodash/fp'], function (exports) { + return require(`./patches/lodash_fp`)(exports, require('lodash')); // eslint-disable-line import/no-dynamic-require +}); + +hook(['lodash'], function (exports) { + return require(`./patches/lodash`)(exports); // eslint-disable-line import/no-dynamic-require +}); diff --git a/src/setup_node_env/patches/lodash.js b/src/setup_node_env/patches/lodash.js new file mode 100644 index 00000000000000..206ebc033553b0 --- /dev/null +++ b/src/setup_node_env/patches/lodash.js @@ -0,0 +1,39 @@ +/* + * 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 (lodash) { + lodash.template = new Proxy(lodash.template, { + apply: function (target, thisArg, args) { + var options; + if (args.length === 1) { + options = { + sourceURL: '', + }; + } else { + options = Object.assign({}, args[1]); + options.sourceURL = (options.sourceURL + '').replace(/\s/g, ' '); + } + + args[1] = options; + return target.apply(thisArg, args); + }, + }); + + return lodash; +}; diff --git a/src/setup_node_env/patches/lodash_fp.js b/src/setup_node_env/patches/lodash_fp.js new file mode 100644 index 00000000000000..937d33ecd24d02 --- /dev/null +++ b/src/setup_node_env/patches/lodash_fp.js @@ -0,0 +1,33 @@ +/* + * 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 (fp, _) { + // per https://github.com/lodash/lodash/wiki/FP-Guide + // > Iteratee arguments are capped to avoid gotchas with variadic iteratees. + // this means that we can't specify thhe options in the second argument... in the proxy + // and just have everything work. Instead, we're going to use the non-FP _.template + // with just the first argument that is specified, and a hardcoded options with sourceURL of '' + fp.template = new Proxy(fp.template, { + apply: function (target, thisArg, args) { + return _.template(args[0], { sourceURL: '' }); + }, + }); + + return fp; +}; diff --git a/test/harden/lodash.js b/test/harden/lodash.js new file mode 100644 index 00000000000000..284bd6d3e3e0f1 --- /dev/null +++ b/test/harden/lodash.js @@ -0,0 +1,103 @@ +/* + * 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 _ = require('lodash'); +const test = require('tape'); + +Object.prototype.sourceURL = '\u2028\u2029\n;global.whoops=true'; // eslint-disable-line no-extend-native + +test.onFinish(() => { + delete Object.prototype.sourceURL; +}); + +test('test setup ok', (t) => { + t.equal({}.sourceURL, '\u2028\u2029\n;global.whoops=true'); + t.end(); +}); + +test(`_.template('<%= foo %>')`, (t) => { + const output = _.template('<%= foo %>')({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +test(`_.template('<%= foo %>', {})`, (t) => { + const output = _.template('<%= foo %>', Object.freeze({}))({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +test(`_.template('<%= data.foo %>', { variable: 'data' })`, (t) => { + const output = _.template('<%= data.foo %>', Object.freeze({ variable: 'data' }))({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +test(`_.template('<%= foo %>', { sourceURL: '/foo/bar' })`, (t) => { + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + const template = _.template('<% throw new Error() %>', Object.freeze({ sourceURL: '/foo/bar' })); + t.plan(2); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.equal(path, '/foo/bar'); + t.equal(global.whoops, undefined); + } +}); + +test(`_.template('<%= foo %>', { sourceURL: '\\u2028\\u2029\\nglobal.whoops=true' })`, (t) => { + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + const template = _.template( + '<% throw new Error() %>', + Object.freeze({ sourceURL: '\u2028\u2029\nglobal.whoops=true' }) + ); + t.plan(2); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.equal(path, 'global.whoops=true'); + t.equal(global.whoops, undefined); + } +}); + +test(`_.template used as an iteratee call(`, (t) => { + const templateStrArr = ['<%= data.foo %>', 'example <%= data.foo %>']; + const output = _.map(templateStrArr, _.template); + + t.equal(output[0]({ data: { foo: 'bar' } }), 'bar'); + t.equal(output[1]({ data: { foo: 'bar' } }), 'example bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +function parsePathFromStack(stack) { + const lines = stack.split('\n'); + // the frame starts at the second line + const frame = lines[1]; + + // the path is in parathensis, and ends with a colon before the line/column numbers + const [, path] = /\(([^:]+)/.exec(frame); + return path; +} diff --git a/test/harden/lodash_fp.js b/test/harden/lodash_fp.js new file mode 100644 index 00000000000000..0f707e2ba88846 --- /dev/null +++ b/test/harden/lodash_fp.js @@ -0,0 +1,114 @@ +/* + * 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 fp = require('lodash/fp'); +const test = require('tape'); + +Object.prototype.sourceURL = '\u2028\u2029\n;global.whoops=true'; // eslint-disable-line no-extend-native + +test.onFinish(() => { + delete Object.prototype.sourceURL; +}); + +test('test setup ok', (t) => { + t.equal({}.sourceURL, '\u2028\u2029\n;global.whoops=true'); + t.end(); +}); + +test(`fp.template('<%= foo %>')`, (t) => { + const output = fp.template('<%= foo %>')({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +test(`fp.template('<%= foo %>', {})`, (t) => { + // fp.template ignores the second argument, this is negligible in this situation since options is an empty object + const output = fp.template('<%= foo %>', Object.freeze({}))({ foo: 'bar' }); + t.equal(output, 'bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +test(`fp.template('<%= data.foo %>', { variable: 'data' })`, (t) => { + // fp.template ignores the second argument, this causes an error to be thrown + t.plan(2); + try { + fp.template('<%= data.foo %>', Object.freeze({ variable: 'data' }))({ foo: 'bar' }); + } catch (err) { + t.equal(err.message, 'data is not defined'); + t.equal(global.whoops, undefined); + } +}); + +test(`fp.template('<%= foo %>', { sourceURL: '/foo/bar' })`, (t) => { + // fp.template ignores the second argument, the sourceURL is ignored + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + // our patching to hard-code the sourceURL and use non-FP _.template does slightly alter the stack-traces but it's negligible + const template = fp.template('<% throw new Error() %>', Object.freeze({ sourceURL: '/foo/bar' })); + t.plan(3); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.match(path, /^eval at /); + t.doesNotMatch(path, /\/foo\/bar/); + t.equal(global.whoops, undefined); + } +}); + +test(`fp.template('<%= foo %>', { sourceURL: '\\u2028\\u2029\\nglobal.whoops=true' })`, (t) => { + // fp.template ignores the second argument, the sourceURL is ignored + // throwing errors in the template and parsing the stack, which is a string, is super ugly, but all I know to do + // our patching to hard-code the sourceURL and use non-FP _.template does slightly alter the stack-traces but it's negligible + const template = fp.template( + '<% throw new Error() %>', + Object.freeze({ sourceURL: '\u2028\u2029\nglobal.whoops=true' }) + ); + t.plan(3); + try { + template(); + } catch (err) { + const path = parsePathFromStack(err.stack); + t.match(path, /^eval at /); + t.doesNotMatch(path, /\/foo\/bar/); + t.equal(global.whoops, undefined); + } +}); + +test(`fp.template used as an iteratee call(`, (t) => { + const templateStrArr = ['<%= data.foo %>', 'example <%= data.foo %>']; + const output = fp.map(fp.template)(templateStrArr); + + t.equal(output[0]({ data: { foo: 'bar' } }), 'bar'); + t.equal(output[1]({ data: { foo: 'bar' } }), 'example bar'); + t.equal(global.whoops, undefined); + t.end(); +}); + +function parsePathFromStack(stack) { + const lines = stack.split('\n'); + // the frame starts at the second line + const frame = lines[1]; + + // the path is in parathensis, and ends with a colon before the line/column numbers + const [, path] = /\(([^:]+)/.exec(frame); + return path; +}