CVE-2024-21508: Insufficient Information (NVD-CWE-noinfo)
value: 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-11T05:15Z
Repository: https://github.com/sidorares/node-mysql2
Language: JavaScript
Files changed: lib/parsers/binary_parser.js, lib/parsers/text_parser.js, test/esm/unit/parsers/big-numbers-strings-sanitization.test.mjs, test/esm/unit/parsers/support-big-numbers-sanitization.test.mjs
Commit date: 2024-04-08 22:38:11 -0300
Commit message: fix(security): improve supportBigNumbers and bigNumberStrings sanitization (#2572)

Fixes a potential RCE attack vulnerability reported by Vsevolod Kokorin (Slonser) of Solidlab
Diff: diff --git a/lib/parsers/binary_parser.js b/lib/parsers/binary_parser.js
index 43160689..b9ee92ff 100644
--- a/lib/parsers/binary_parser.js
+++ b/lib/parsers/binary_parser.js
@@ -10,13 +10,16 @@ const typeNames = [];
 for (const t in Types) {
   typeNames[Types[t]] = t;
 }
 
 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;
   switch (field.columnType) {
     case Types.TINY:
diff --git a/lib/parsers/text_parser.js b/lib/parsers/text_parser.js
index 49a128c0..23d0d386 100644
--- a/lib/parsers/text_parser.js
+++ b/lib/parsers/text_parser.js
@@ -10,13 +10,16 @@ const typeNames = [];
 for (const t in Types) {
   typeNames[Types[t]] = t;
 }
 
 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;
 
   switch (type) {
     case Types.TINY:
@@ -83,37 +86,37 @@ function compile(fields, options, config) {
       type: typeNames[field.columnType],
       length: field.columnLength,
       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();
-      }
+      },
     };
   }
 
   const parserFn = genFunc();
 
   /* 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) {');
   // node-mysql typeCast compatibility wrapper
   // see https://github.com/mysqljs/mysql/blob/96fdd0566b654436624e2375c7b6604b1f50f825/lib/protocol/packets/Field.js
@@ -125,26 +128,26 @@ function compile(fields, options, config) {
   }
   parserFn('}');
 
   // 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])}] = {};`);
     }
   }
 
   let lvalue = '';
@@ -152,11 +155,11 @@ function compile(fields, options, config) {
   for (let i = 0; i < fields.length; i++) {
     fieldName = helpers.srcEscape(fields[i].name);
     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}]`;
     } else if (options.rowsAsArray) {
       lvalue = `result[${i.toString(10)}]`;
@@ -170,15 +173,17 @@ function compile(fields, options, config) {
       const readCode = readCodeFor(
         fields[i].columnType,
         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};`);
       }
     }
   }
 
@@ -191,15 +196,15 @@ function compile(fields, options, config) {
   /* eslint-enable no-unexpected-multiline */
 
   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();
 }
 
 function getTextParser(fields, options, config) {
diff --git a/test/esm/unit/parsers/big-numbers-strings-sanitization.test.mjs b/test/esm/unit/parsers/big-numbers-strings-sanitization.test.mjs
new file mode 100644
index 00000000..09cc6a2d
--- /dev/null
+++ b/test/esm/unit/parsers/big-numbers-strings-sanitization.test.mjs
@@ -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();
+});
diff --git a/test/esm/unit/parsers/support-big-numbers-sanitization.test.mjs b/test/esm/unit/parsers/support-big-numbers-sanitization.test.mjs
new file mode 100644
index 00000000..174ed76f
--- /dev/null
+++ b/test/esm/unit/parsers/support-big-numbers-sanitization.test.mjs
@@ -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();
+});

Vulnerable code:   const supportBigNumbers =
    options.supportBigNumbers || config.supportBigNumbers;
  const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings;
  const supportBigNumbers =
    options.supportBigNumbers || config.supportBigNumbers;
  const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings;
      string: function(encoding = field.encoding) {
          console.warn(`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``);
      buffer: function() {
      geometry: function() {
      }
  parserFn('(function () {')(
    'return class TextRow {'
  );
  parserFn("this.packet = packet;");
    parserFn("const result = {};");
    for (let i=0; i < fields.length; i++) {
    for (let i=0; i < resultTablesArray.length; i++) {
        fields[i].table + options.nestTables + fields[i].name
        options
        parserFn(`${lvalue} = options.typeCast(this.wrap${i}, function() { return ${readCode} });`);
      }  else {
      parserFn.toString()
    return parserFn.toFunction({wrap});
Fixed code:   const supportBigNumbers = Boolean(
    options.supportBigNumbers || config.supportBigNumbers,
  );
  const bigNumberStrings = Boolean(
    options.bigNumberStrings || config.bigNumberStrings,
  );
  const supportBigNumbers = Boolean(
    options.supportBigNumbers || config.supportBigNumbers,
  );
  const bigNumberStrings = Boolean(
    options.bigNumberStrings || config.bigNumberStrings,
  );
      string: function (encoding = field.encoding) {
          console.warn(
            `typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``,
          );
      buffer: function () {
      geometry: function () {
      },
  parserFn('(function () {')('return class TextRow {');
  parserFn('this.packet = packet;');
    parserFn('const result = {};');
    for (let i = 0; i < fields.length; i++) {
    for (let i = 0; i < resultTablesArray.length; i++) {
        fields[i].table + options.nestTables + fields[i].name,
        options,
        parserFn(
          `${lvalue} = options.typeCast(this.wrap${i}, function() { return ${readCode} });`,
        );
      } else {
      parserFn.toString(),
    return parserFn.toFunction({ wrap });
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();
});
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();
});