diff --git a/.nycrc b/.nycrc index 04e485a286..a0d7e75399 100644 --- a/.nycrc +++ b/.nycrc @@ -5,7 +5,7 @@ "reporter": ["text", "lcov", "cobertura"], "statements": 88, "branches": 84, - "functions": 78, + "functions": 77, "lines": 88, "checkCoverage": true, "clean": true diff --git a/lib/parsers/binary_parser.js b/lib/parsers/binary_parser.js index b9ee92ffe6..26ac7893e8 100644 --- a/lib/parsers/binary_parser.js +++ b/lib/parsers/binary_parser.js @@ -122,7 +122,13 @@ function compile(fields, options, config) { if (options.rowsAsArray) { parserFn(`const result = new Array(${fields.length});`); } else { - parserFn('const result = {};'); + parserFn('const result = Object.create(null);'); + parserFn(`Object.defineProperty(result, "constructor", { + value: Object.create(null), + writable: false, + configurable: false, + enumerable: false + });`); } // Global typeCast @@ -154,7 +160,9 @@ function compile(fields, options, config) { )}]`; } else if (options.nestTables === true) { tableName = helpers.srcEscape(fields[i].table); - parserFn(`if (!result[${tableName}]) result[${tableName}] = {};`); + parserFn( + `if (!result[${tableName}]) result[${tableName}] = Object.create(null);`, + ); lvalue = `result[${tableName}][${fieldName}]`; } else if (options.rowsAsArray) { lvalue = `result[${i.toString(10)}]`; diff --git a/lib/parsers/text_parser.js b/lib/parsers/text_parser.js index 23d0d38629..e757b45623 100644 --- a/lib/parsers/text_parser.js +++ b/lib/parsers/text_parser.js @@ -111,9 +111,6 @@ function compile(fields, options, config) { const parserFn = genFunc(); - /* eslint-disable no-trailing-spaces */ - /* eslint-disable no-spaced-func */ - /* eslint-disable no-unexpected-multiline */ parserFn('(function () {')('return class TextRow {'); // constructor method @@ -134,7 +131,13 @@ function compile(fields, options, config) { if (options.rowsAsArray) { parserFn(`const result = new Array(${fields.length});`); } else { - parserFn('const result = {};'); + parserFn('const result = Object.create(null);'); + parserFn(`Object.defineProperty(result, "constructor", { + value: Object.create(null), + writable: false, + configurable: false, + enumerable: false + });`); } const resultTables = {}; @@ -146,7 +149,9 @@ function compile(fields, options, config) { } resultTablesArray = Object.keys(resultTables); for (let i = 0; i < resultTablesArray.length; i++) { - parserFn(`result[${helpers.srcEscape(resultTablesArray[i])}] = {};`); + parserFn( + `result[${helpers.srcEscape(resultTablesArray[i])}] = Object.create(null);`, + ); } } @@ -191,10 +196,6 @@ function compile(fields, options, config) { parserFn('}'); parserFn('};')('})()'); - /* eslint-enable no-trailing-spaces */ - /* eslint-enable no-spaced-func */ - /* eslint-enable no-unexpected-multiline */ - if (config.debug) { helpers.printDebugWithCode( 'Compiled text protocol row parser', diff --git a/test/common.test.cjs b/test/common.test.cjs index 88b3750244..e1ffa9720a 100644 --- a/test/common.test.cjs +++ b/test/common.test.cjs @@ -105,6 +105,7 @@ exports.createConnection = function (args) { typeCast: args && args.typeCast, namedPlaceholders: args && args.namedPlaceholders, connectTimeout: args && args.connectTimeout, + nestTables: args && args.nestTables, ssl: (args && args.ssl) ?? config.ssl, }; diff --git a/test/esm/unit/parsers/prototype-binary-results.test.mjs b/test/esm/unit/parsers/prototype-binary-results.test.mjs new file mode 100644 index 0000000000..a33eec077b --- /dev/null +++ b/test/esm/unit/parsers/prototype-binary-results.test.mjs @@ -0,0 +1,72 @@ +import { test, describe, assert } from 'poku'; +import { createConnection, describeOptions } from '../../../common.test.cjs'; + +const connection = createConnection().promise(); + +describe('Binary Parser: Prototype Sanitization', describeOptions); + +Promise.all([ + test(async () => { + const expected = [{}]; + expected[0].test = 2; + + const [results] = await connection.query('SELECT 1+1 AS `test`'); + + assert.notDeepStrictEqual( + results, + expected, + `Ensure "results" doesn't contain a standard object ({})`, + ); + }), + test(async () => { + const expected = [Object.create(null)]; + expected[0].test = 2; + + const [results] = await connection.execute('SELECT 1+1 AS `test`'); + + assert.deepStrictEqual(results, expected, 'Ensure clean object "results"'); + assert.strictEqual( + Object.getPrototypeOf(results[0]), + null, + 'Ensure clean properties in results items', + ); + assert.strictEqual( + typeof results[0].toString, + 'undefined', + 'Re-check prototypes (manually) in results columns', + ); + assert.strictEqual( + typeof results[0].test.toString, + 'function', + 'Ensure that the end-user is able to use prototypes', + ); + assert.strictEqual( + results[0].test.toString(), + '2', + 'Ensure that the end-user is able to use prototypes (manually): toString', + ); + assert.strictEqual( + results[0].test.toFixed(2), + '2.00', + 'Ensure that the end-user is able to use prototypes (manually): toFixed', + ); + + results[0].customProp = true; + assert.strictEqual( + results[0].customProp, + true, + 'Ensure that the end-user is able to use custom props', + ); + }), + test(async () => { + const [result] = await connection.execute('SET @1 = 1;'); + + assert.strictEqual( + result.constructor.name, + 'ResultSetHeader', + 'Ensure constructor name in result object', + ); + }), +]).then(async () => { + await connection.end(); +}); diff --git a/test/esm/unit/parsers/prototype-text-results.test.mjs b/test/esm/unit/parsers/prototype-text-results.test.mjs new file mode 100644 index 0000000000..99a6a69312 --- /dev/null +++ b/test/esm/unit/parsers/prototype-text-results.test.mjs @@ -0,0 +1,72 @@ +import { test, describe, assert } from 'poku'; +import { createConnection, describeOptions } from '../../../common.test.cjs'; + +const connection = createConnection().promise(); + +describe('Text Parser: Prototype Sanitization', describeOptions); + +Promise.all([ + test(async () => { + const expected = [{}]; + expected[0].test = 2; + + const [results] = await connection.query('SELECT 1+1 AS `test`'); + + assert.notDeepStrictEqual( + results, + expected, + `Ensure "results" doesn't contain a standard object ({})`, + ); + }), + test(async () => { + const expected = [Object.create(null)]; + expected[0].test = 2; + + const [results] = await connection.query('SELECT 1+1 AS `test`'); + + assert.deepStrictEqual(results, expected, 'Ensure clean object "results"'); + assert.strictEqual( + Object.getPrototypeOf(results[0]), + null, + 'Ensure clean properties in results items', + ); + assert.strictEqual( + typeof results[0].toString, + 'undefined', + 'Re-check prototypes (manually) in results columns', + ); + assert.strictEqual( + typeof results[0].test.toString, + 'function', + 'Ensure that the end-user is able to use prototypes', + ); + assert.strictEqual( + results[0].test.toString(), + '2', + 'Ensure that the end-user is able to use prototypes (manually): toString', + ); + assert.strictEqual( + results[0].test.toFixed(2), + '2.00', + 'Ensure that the end-user is able to use prototypes (manually): toFixed', + ); + + results[0].customProp = true; + assert.strictEqual( + results[0].customProp, + true, + 'Ensure that the end-user is able to use custom props', + ); + }), + test(async () => { + const [result] = await connection.query('SET @1 = 1;'); + + assert.strictEqual( + result.constructor.name, + 'ResultSetHeader', + 'Ensure constructor name in result object', + ); + }), +]).then(async () => { + await connection.end(); +});