CVE-2024-21509 (published 2024-04-10T05:15Z)

Versions of the package mysql2 before 3.9.4 are vulnerable to Prototype Poisoning due to insecure results object creation and improper user input sanitization passed through parserFn in text_parser.js and binary_parser.js.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/sidorares/node-mysql2
commit_details: repo_url: https://github.com/sidorares/node-mysql2
author: Weslley Araújo
author_date: 2024-04-09 04:02:39-03:00
msg: fix(security): improve results object creation (#2574)

Fixes a potential Prototype Pollution attack vulnerability reported by Vsevolod Kokorin (Slonser) of Solidlab
repository: repo_url: https://github.com/sidorares/node-mysql2
repo_name: sidorares/node-mysql2
description: :zap: fast mysqljs/mysql compatible mysql driver for node.js
repo_language: JavaScript
owner: sidorares
file_changes: filename: .nycrc
change_type: ModificationType.MODIFY
diff: @@ -5,7 +5,7 @@
   "reporter": ["text", "lcov", "cobertura"],
   "statements": 88,
   "branches": 84,
-  "functions": 78,
+  "functions": 77,
   "lines": 88,
   "checkCoverage": true,
   "clean": true

programming_language: JavaScript
filename: binary_parser.js
change_type: ModificationType.MODIFY
diff: @@ -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)}]`;

programming_language: JavaScript
filename: text_parser.js
change_type: ModificationType.MODIFY
diff: @@ -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',

programming_language: JavaScript
filename: common.test.cjs
change_type: ModificationType.MODIFY
diff: @@ -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,
   };
 

programming_language: JavaScript
filename: prototype-binary-results.test.mjs
change_type: ModificationType.ADD
diff: @@ -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();
+});

programming_language: TypeScript
filename: prototype-text-results.test.mjs
change_type: ModificationType.ADD
diff: @@ -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();
+});

programming_language: TypeScript