CVE-2024-21508 (published 2024-04-11T05:15Z)

Versions of the package mysql2 before 3.9.4 are vulnerable to Remote Code Execution (RCE) via the readCodeFor function due to improper validation of the supportBigNumbers and bigNumberStrings values.


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-08 22:38:11-03:00
msg: fix(security): improve supportBigNumbers and bigNumberStrings sanitization (#2572)

Fixes a potential RCE 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: binary_parser.js
change_type: ModificationType.MODIFY
diff: @@ -12,9 +12,12 @@ for (const t in Types) {
 }
 
 function readCodeFor(field, config, options, fieldNum) {
-  const supportBigNumbers =
-    options.supportBigNumbers || config.supportBigNumbers;
-  const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings;
+  const supportBigNumbers = Boolean(
+    options.supportBigNumbers || config.supportBigNumbers,
+  );
+  const bigNumberStrings = Boolean(
+    options.bigNumberStrings || config.bigNumberStrings,
+  );
   const timezone = options.timezone || config.timezone;
   const dateStrings = options.dateStrings || config.dateStrings;
   const unsigned = field.flags & FieldFlags.UNSIGNED;

programming_language: JavaScript
filename: text_parser.js
change_type: ModificationType.MODIFY
diff: @@ -12,9 +12,12 @@ for (const t in Types) {
 }
 
 function readCodeFor(type, charset, encodingExpr, config, options) {
-  const supportBigNumbers =
-    options.supportBigNumbers || config.supportBigNumbers;
-  const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings;
+  const supportBigNumbers = Boolean(
+    options.supportBigNumbers || config.supportBigNumbers,
+  );
+  const bigNumberStrings = Boolean(
+    options.bigNumberStrings || config.bigNumberStrings,
+  );
   const timezone = options.timezone || config.timezone;
   const dateStrings = options.dateStrings || config.dateStrings;
 
@@ -85,22 +88,24 @@ function compile(fields, options, config) {
       db: field.schema,
       table: field.table,
       name: field.name,
-      string: function(encoding = field.encoding) {
+      string: function (encoding = field.encoding) {
         if (field.columnType === Types.JSON && encoding === field.encoding) {
           // Since for JSON columns mysql always returns charset 63 (BINARY),
           // we have to handle it according to JSON specs and use "utf8",
           // see https://github.com/sidorares/node-mysql2/issues/1661
-          console.warn(`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``);
+          console.warn(
+            `typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``,
+          );
         }
 
         return _this.packet.readLengthCodedString(encoding);
       },
-      buffer: function() {
+      buffer: function () {
         return _this.packet.readLengthCodedBuffer();
       },
-      geometry: function() {
+      geometry: function () {
         return _this.packet.parseGeometryValue();
-      }
+      },
     };
   }
 
@@ -109,9 +114,7 @@ function compile(fields, options, config) {
   /* eslint-disable no-trailing-spaces */
   /* eslint-disable no-spaced-func */
   /* eslint-disable no-unexpected-multiline */
-  parserFn('(function () {')(
-    'return class TextRow {'
-  );
+  parserFn('(function () {')('return class TextRow {');
 
   // constructor method
   parserFn('constructor(fields) {');
@@ -127,22 +130,22 @@ function compile(fields, options, config) {
 
   // next method
   parserFn('next(packet, fields, options) {');
-  parserFn("this.packet = packet;");
+  parserFn('this.packet = packet;');
   if (options.rowsAsArray) {
     parserFn(`const result = new Array(${fields.length});`);
   } else {
-    parserFn("const result = {};");
+    parserFn('const result = {};');
   }
 
   const resultTables = {};
   let resultTablesArray = [];
 
   if (options.nestTables === true) {
-    for (let i=0; i < fields.length; i++) {
+    for (let i = 0; i < fields.length; i++) {
       resultTables[fields[i].table] = 1;
     }
     resultTablesArray = Object.keys(resultTables);
-    for (let i=0; i < resultTablesArray.length; i++) {
+    for (let i = 0; i < resultTablesArray.length; i++) {
       parserFn(`result[${helpers.srcEscape(resultTablesArray[i])}] = {};`);
     }
   }
@@ -154,7 +157,7 @@ function compile(fields, options, config) {
     parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`);
     if (typeof options.nestTables === 'string') {
       lvalue = `result[${helpers.srcEscape(
-        fields[i].table + options.nestTables + fields[i].name
+        fields[i].table + options.nestTables + fields[i].name,
       )}]`;
     } else if (options.nestTables === true) {
       lvalue = `result[${helpers.srcEscape(fields[i].table)}][${fieldName}]`;
@@ -172,11 +175,13 @@ function compile(fields, options, config) {
         fields[i].characterSet,
         encodingExpr,
         config,
-        options
+        options,
       );
       if (typeof options.typeCast === 'function') {
-        parserFn(`${lvalue} = options.typeCast(this.wrap${i}, function() { return ${readCode} });`);
-      }  else {
+        parserFn(
+          `${lvalue} = options.typeCast(this.wrap${i}, function() { return ${readCode} });`,
+        );
+      } else {
         parserFn(`${lvalue} = ${readCode};`);
       }
     }
@@ -193,11 +198,11 @@ function compile(fields, options, config) {
   if (config.debug) {
     helpers.printDebugWithCode(
       'Compiled text protocol row parser',
-      parserFn.toString()
+      parserFn.toString(),
     );
   }
   if (typeof options.typeCast === 'function') {
-    return parserFn.toFunction({wrap});
+    return parserFn.toFunction({ wrap });
   }
   return parserFn.toFunction();
 }

programming_language: JavaScript
filename: big-numbers-strings-sanitization.test.mjs
change_type: ModificationType.ADD
diff: @@ -0,0 +1,66 @@
+import { describe, test, assert } from 'poku';
+import { createConnection, describeOptions } from '../../../common.test.cjs';
+
+const connection = createConnection().promise();
+
+const sql = 'SELECT 9007199254740991+100 AS `total`';
+
+describe('bigNumberStrings Sanitization', describeOptions);
+
+Promise.all([
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: true,
+      bigNumberStrings: true,
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'string',
+      'Valid bigNumberStrings enabled',
+    );
+  }),
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: false,
+      bigNumberStrings: false,
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'number',
+      'Valid bigNumberStrings disabled',
+    );
+  }),
+
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: 'text',
+      bigNumberStrings: 'text',
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'string',
+      'bigNumberStrings as a random string should be enabled',
+    );
+  }),
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: '',
+      bigNumberStrings: '',
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'number',
+      'bigNumberStrings as an empty string should be disabled',
+    );
+  }),
+]).then(async () => {
+  await connection.end();
+});

programming_language: TypeScript
filename: support-big-numbers-sanitization.test.mjs
change_type: ModificationType.ADD
diff: @@ -0,0 +1,62 @@
+import { describe, test, assert } from 'poku';
+import { createConnection, describeOptions } from '../../../common.test.cjs';
+
+const connection = createConnection().promise();
+
+const sql = 'SELECT 9007199254740991+100 AS `total`';
+
+describe('supportBigNumbers Sanitization', describeOptions);
+
+Promise.all([
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: true,
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'string',
+      'Valid supportBigNumbers enabled',
+    );
+  }),
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: false,
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'number',
+      'Valid supportBigNumbers disabled',
+    );
+  }),
+
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: 'text',
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'string',
+      'supportBigNumbers as a random string should be enabled',
+    );
+  }),
+  test(async () => {
+    const [results] = await connection.query({
+      sql,
+      supportBigNumbers: '',
+    });
+
+    assert.strictEqual(
+      typeof results[0].total,
+      'number',
+      'supportBigNumbers as an empty string should be disabled',
+    );
+  }),
+]).then(async () => {
+  await connection.end();
+});

programming_language: TypeScript