CVE-2024-21507
Description
mysql2 before 3.9.3 allows cache poisoning via colon injection in keyFromFields, enabling attackers to manipulate cached query results.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
mysql2 before 3.9.3 allows cache poisoning via colon injection in keyFromFields, enabling attackers to manipulate cached query results.
Vulnerability
Versions of the mysql2 package before 3.9.3 are vulnerable to Improper Input Validation in the keyFromFields function [1]. An attacker can inject a colon (:) character within a value of an attacker-crafted key, leading to cache poisoning [1].
Exploitation
Exploitation requires the attacker to control the key fields passed to the library, which can occur in scenarios where user-defined database connections are used, as described in security research [2]. The attacker can craft a malicious key value containing a colon to poison the internal cache used for query result mapping [2].
Impact
Cache poisoning can cause the application to return incorrect or malicious data from previous queries, potentially affecting data integrity and leading to further attacks if cached results are reused across users or sessions [1][2].
Mitigation
The issue has been fixed in mysql2 version 3.9.3 via a pull request that improves cache key serialization [3]. Users are advised to update to the latest version [4]. No known workarounds are available.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mysql2npm | < 3.9.3 | 3.9.3 |
Affected products
6- mysql2/mysql2description
- osv-coords5 versionspkg:apk/chainguard/sqlpadpkg:apk/chainguard/sqlpad-compatpkg:apk/wolfi/sqlpadpkg:apk/wolfi/sqlpad-compatpkg:npm/mysql2
< 7.4.1-r4+ 4 more
- (no CPE)range: < 7.4.1-r4
- (no CPE)range: < 7.4.1-r4
- (no CPE)range: < 7.4.1-r4
- (no CPE)range: < 7.4.1-r4
- (no CPE)range: < 3.9.3
Patches
10d54b0ca6498fix(cache): improve cache key serialization (#2424)
2 files changed · +518 −15
lib/parsers/parser_cache.js+28 −15 modified@@ -3,26 +3,38 @@ const LRU = require('lru-cache').default; const parserCache = new LRU({ - max: 15000 + max: 15000, }); function keyFromFields(type, fields, options, config) { - let res = - `${type}` + - `/${typeof options.nestTables}` + - `/${options.nestTables}` + - `/${options.rowsAsArray}` + - `/${options.supportBigNumbers || config.supportBigNumbers}` + - `/${options.bigNumberStrings || config.bigNumberStrings}` + - `/${typeof options.typeCast}` + - `/${options.timezone || config.timezone}` + - `/${options.decimalNumbers}` + - `/${options.dateStrings}`; + const res = [ + type, + typeof options.nestTables, + options.nestTables, + Boolean(options.rowsAsArray), + Boolean(options.supportBigNumbers || config.supportBigNumbers), + Boolean(options.bigNumberStrings || config.bigNumberStrings), + typeof options.typeCast, + options.timezone || config.timezone, + Boolean(options.decimalNumbers), + options.dateStrings, + ]; + for (let i = 0; i < fields.length; ++i) { const field = fields[i]; - res += `/${field.name}:${field.columnType}:${field.length}:${field.schema}:${field.table}:${field.flags}:${field.characterSet}`; + + res.push([ + field.name, + field.columnType, + field.length, + field.schema, + field.table, + field.flags, + field.characterSet, + ]); } - return res; + + return JSON.stringify(res, null, 0); } function getParser(type, fields, options, config, compiler) { @@ -49,5 +61,6 @@ function clearCache() { module.exports = { getParser: getParser, setMaxCache: setMaxCache, - clearCache: clearCache + clearCache: clearCache, + _keyFromFields: keyFromFields, };
test/unit/parsers/cache-key-serialization.js+490 −0 added@@ -0,0 +1,490 @@ +'use strict'; + +const assert = require('assert'); +const _keyFromFields = + require('../../../lib/parsers/parser_cache.js')._keyFromFields; + +// Invalid +const test1 = { + type: undefined, + fields: [ + { + name: undefined, + columnType: undefined, + length: undefined, + schema: undefined, + table: undefined, + flags: undefined, + characterSet: undefined, + }, + ], + options: { + nestTables: undefined, + rowsAsArray: undefined, + supportBigNumbers: undefined, + bigNumberStrings: undefined, + typeCast: undefined, + timezone: undefined, + decimalNumbers: undefined, + dateStrings: undefined, + }, + config: { + supportBigNumbers: undefined, + bigNumberStrings: undefined, + timezone: undefined, + }, +}; + +// Invalid, except for `config` (global overwriting) +const test2 = { + type: undefined, + fields: [ + { + name: undefined, + columnType: undefined, + length: undefined, + schema: undefined, + table: undefined, + flags: undefined, + characterSet: undefined, + }, + ], + options: { + nestTables: undefined, + rowsAsArray: undefined, + supportBigNumbers: undefined, + bigNumberStrings: undefined, + typeCast: undefined, + timezone: undefined, + decimalNumbers: undefined, + dateStrings: undefined, + }, + config: { + supportBigNumbers: false, + bigNumberStrings: false, + timezone: 'local', + }, +}; + +// Invalid, except for options +const test3 = { + type: undefined, + fields: [ + { + name: undefined, + columnType: undefined, + length: undefined, + schema: undefined, + table: undefined, + flags: undefined, + characterSet: undefined, + }, + ], + options: { + nestTables: '', + rowsAsArray: false, + supportBigNumbers: false, + bigNumberStrings: false, + typeCast: true, + timezone: 'local', + decimalNumbers: false, + dateStrings: false, + }, + config: { + supportBigNumbers: undefined, + bigNumberStrings: undefined, + timezone: undefined, + }, +}; + +// Based on results of `SELECT * FROM test WHERE value = ?` +const test4 = { + type: 'binary', + fields: [ + { + name: 'id', + columnType: '3', + length: undefined, + schema: 'test', + table: 'test', + flags: '16899', + characterSet: '63', + }, + { + name: 'value', + columnType: '246', + length: undefined, + schema: 'test', + table: 'test', + flags: '0', + characterSet: '63', + }, + ], + options: { + nestTables: false, + rowsAsArray: false, + supportBigNumbers: false, + bigNumberStrings: false, + typeCast: true, + timezone: 'local', + decimalNumbers: false, + dateStrings: 'DATETIME', + }, + config: { + supportBigNumbers: undefined, + bigNumberStrings: undefined, + timezone: undefined, + }, +}; + +// Same from test4, but with invalid booleans need to reach out the same key +const test5 = { + type: 'binary', + fields: [ + { + name: 'id', + columnType: '3', + length: undefined, + schema: 'test', + table: 'test', + flags: '16899', + characterSet: '63', + }, + { + name: 'value', + columnType: '246', + length: undefined, + schema: 'test', + table: 'test', + flags: '0', + characterSet: '63', + }, + ], + options: { + nestTables: false, + rowsAsArray: undefined, + supportBigNumbers: undefined, + bigNumberStrings: undefined, + typeCast: true, + timezone: 'local', + decimalNumbers: undefined, + dateStrings: 'DATETIME', + }, + config: { + supportBigNumbers: undefined, + bigNumberStrings: undefined, + timezone: undefined, + }, +}; + +// Forcing delimiters on strings fields +// Checking for quotes escape +const test6 = { + type: 'binary', + fields: [ + { + name: ':', + columnType: '©', + length: undefined, + schema: '/', + table: ',', + flags: '_', + characterSet: '❌', + }, + ], + options: { + nestTables: false, + rowsAsArray: true, + supportBigNumbers: true, + bigNumberStrings: true, + typeCast: true, + timezone: '""`\'', + decimalNumbers: true, + dateStrings: '#', + }, + config: { + supportBigNumbers: undefined, + bigNumberStrings: undefined, + timezone: undefined, + }, +}; + +// valid with `true` on booleans +const test7 = { + type: 'binary', + fields: [ + { + name: 'id', + columnType: '3', + length: undefined, + schema: 'test', + table: 'test', + flags: '16899', + characterSet: '63', + }, + { + name: 'value', + columnType: '246', + length: undefined, + schema: 'test', + table: 'test', + flags: '0', + characterSet: '63', + }, + ], + options: { + nestTables: true, + rowsAsArray: true, + supportBigNumbers: true, + bigNumberStrings: true, + typeCast: true, + timezone: 'local', + decimalNumbers: true, + dateStrings: 'DATETIME', + }, + config: { + supportBigNumbers: true, + bigNumberStrings: true, + timezone: true, + }, +}; + +// Expects the same result from test7, but using valid values instead of `true` on booleans fields +const test8 = { + type: 'binary', + fields: [ + { + name: 'id', + columnType: '3', + length: undefined, + schema: 'test', + table: 'test', + flags: '16899', + characterSet: '63', + }, + { + name: 'value', + columnType: '246', + length: undefined, + schema: 'test', + table: 'test', + flags: '0', + characterSet: '63', + }, + ], + options: { + nestTables: true, + rowsAsArray: 2, + supportBigNumbers: 'yes', + bigNumberStrings: [], + typeCast: true, + timezone: 'local', + decimalNumbers: { + a: null, + }, + dateStrings: 'DATETIME', + }, + config: { + supportBigNumbers: true, + bigNumberStrings: true, + timezone: true, + }, +}; + +// Invalid: checking function parser in wrong fields, expecting to be `null` +const test9 = { + type: 'binary', + fields: [ + { + name: 'id', + columnType: '3', + length: undefined, + schema: 'test', + table: 'test', + flags: '16899', + characterSet: '63', + }, + ], + options: { + nestTables: false, + rowsAsArray: false, + supportBigNumbers: false, + // Expected: true + bigNumberStrings: (_, next) => next(), + // Expected: "function" + typeCast: (_, next) => next(), + timezone: 'local', + decimalNumbers: false, + // Expected: null + dateStrings: (_, next) => next(), + }, + config: { + supportBigNumbers: undefined, + bigNumberStrings: undefined, + timezone: undefined, + }, +}; + +const result1 = _keyFromFields( + test1.type, + test1.fields, + test1.options, + test1.config, +); +const result2 = _keyFromFields( + test2.type, + test2.fields, + test2.options, + test2.config, +); +const result3 = _keyFromFields( + test3.type, + test3.fields, + test3.options, + test3.config, +); +const result4 = _keyFromFields( + test4.type, + test4.fields, + test4.options, + test4.config, +); +const result5 = _keyFromFields( + test5.type, + test5.fields, + test5.options, + test5.config, +); +const result6 = _keyFromFields( + test6.type, + test6.fields, + test6.options, + test6.config, +); +const result7 = _keyFromFields( + test7.type, + test7.fields, + test7.options, + test7.config, +); +const result8 = _keyFromFields( + test8.type, + test8.fields, + test8.options, + test8.config, +); +const result9 = _keyFromFields( + test9.type, + test9.fields, + test9.options, + test9.config, +); + +assert.deepStrictEqual( + result1, + '[null,"undefined",null,false,false,false,"undefined",null,false,null,[null,null,null,null,null,null,null]]', +); +assert(JSON.parse(result1)); + +assert.deepStrictEqual( + result2, + '[null,"undefined",null,false,false,false,"undefined","local",false,null,[null,null,null,null,null,null,null]]', +); +assert(JSON.parse(result2)); + +assert.deepStrictEqual( + result3, + '[null,"string","",false,false,false,"boolean","local",false,false,[null,null,null,null,null,null,null]]', +); +assert(JSON.parse(result3)); + +assert.deepStrictEqual( + result4, + '["binary","boolean",false,false,false,false,"boolean","local",false,"DATETIME",["id","3",null,"test","test","16899","63"],["value","246",null,"test","test","0","63"]]', +); +assert(JSON.parse(result4)); + +assert.deepStrictEqual(result4, result5); +assert(JSON.parse(result5)); + +assert.deepStrictEqual( + result6, + '["binary","boolean",false,true,true,true,"boolean","\\"\\"`\'",true,"#",[":","©",null,"/",",","_","❌"]]', +); +// Ensuring that JSON is valid with invalid delimiters +assert(JSON.parse(result6)); + +assert.deepStrictEqual( + result7, + '["binary","boolean",true,true,true,true,"boolean","local",true,"DATETIME",["id","3",null,"test","test","16899","63"],["value","246",null,"test","test","0","63"]]', +); +assert(JSON.parse(result7)); + +assert.deepStrictEqual(result7, result8); +assert(JSON.parse(result8)); + +assert.deepStrictEqual( + result9, + '["binary","boolean",false,false,false,true,"function","local",false,null,["id","3",null,"test","test","16899","63"]]', +); +assert(JSON.parse(result9)); +assert(JSON.parse(result9)[5] === true); +assert(JSON.parse(result9)[6] === 'function'); +assert(JSON.parse(result9)[9] === null); + +// Testing twice all existent tests needs to return 7 keys, since two of them expects to be the same +assert( + Array.from( + new Set([ + _keyFromFields(test1.type, test1.fields, test1.options, test1.config), + _keyFromFields(test1.type, test1.fields, test1.options, test1.config), + _keyFromFields(test2.type, test2.fields, test2.options, test2.config), + _keyFromFields(test2.type, test2.fields, test2.options, test2.config), + _keyFromFields(test3.type, test3.fields, test3.options, test3.config), + _keyFromFields(test3.type, test3.fields, test3.options, test3.config), + _keyFromFields(test4.type, test4.fields, test4.options, test4.config), + _keyFromFields(test4.type, test4.fields, test4.options, test4.config), + _keyFromFields(test5.type, test5.fields, test5.options, test5.config), + _keyFromFields(test5.type, test5.fields, test5.options, test5.config), + _keyFromFields(test6.type, test6.fields, test6.options, test6.config), + _keyFromFields(test6.type, test6.fields, test6.options, test6.config), + _keyFromFields(test7.type, test7.fields, test7.options, test7.config), + _keyFromFields(test7.type, test7.fields, test7.options, test7.config), + _keyFromFields(test8.type, test8.fields, test8.options, test8.config), + _keyFromFields(test8.type, test8.fields, test8.options, test8.config), + _keyFromFields(test9.type, test9.fields, test9.options, test9.config), + _keyFromFields(test9.type, test9.fields, test9.options, test9.config), + ]), + ).length === 7, +); + +const stringify = JSON.stringify; + +// Overwriting the native `JSON.stringify` +JSON.stringify = (value, replacer, space = 8) => stringify(value, replacer, space); + +// Testing twice all existent tests needs to return 7 keys, since two of them expects to be the same +assert( + Array.from( + new Set([ + result1, + _keyFromFields(test1.type, test1.fields, test1.options, test1.config), + result2, + _keyFromFields(test2.type, test2.fields, test2.options, test2.config), + result3, + _keyFromFields(test3.type, test3.fields, test3.options, test3.config), + result4, + _keyFromFields(test4.type, test4.fields, test4.options, test4.config), + result5, + _keyFromFields(test5.type, test5.fields, test5.options, test5.config), + result6, + _keyFromFields(test6.type, test6.fields, test6.options, test6.config), + result7, + _keyFromFields(test7.type, test7.fields, test7.options, test7.config), + result8, + _keyFromFields(test8.type, test8.fields, test8.options, test8.config), + result9, + _keyFromFields(test9.type, test9.fields, test9.options, test9.config), + ]), + ).length === 7, +);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-mqr2-w7wj-jjgrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-21507ghsaADVISORY
- blog.slonser.info/posts/mysql2-attacker-configurationghsaWEB
- github.com/sidorares/node-mysql2/commit/0d54b0ca6498c823098426038162ef10df02c818ghsaWEB
- github.com/sidorares/node-mysql2/pull/2424ghsaWEB
- security.snyk.io/vuln/SNYK-JS-MYSQL2-6591300ghsaWEB
- blog.slonser.info/posts/mysql2-attacker-configuration/mitre
News mentions
0No linked articles in our index yet.