
CVE-2023-25813の概念実証と技術分析。Sequelize ORMの6.19.1より前のバージョンに存在するSQLインジェクションの脆弱性であり、悪用の詳細とパッチ情報を含みます。
replacements オプションに渡された入力値が適切にエスケープされないため、特定のクエリ構成において SQL インジェクションが発生する可能性があります。Sequelize は、Node.js で使用できる ORM (オブジェクトリレーショナルマッピング) ライブラリの一つです。ORM は、オブジェクトとリレーショナルデータベース間の相互変換を自動化する技術であり、SQL クエリを直接記述しなくても、オブジェクトを介してデータベースレコードの検索、作成、更新、削除が可能です。
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
replacements は、Sequelize で生の SQL クエリまたは ORM メソッドを使用する際に、ユーザー入力を SQL に安全に注入するためのバインディングメカニズムです。これは SQL インジェクションを防ぐための主要な手段として使用されます。
// 位置ベースのバインディング
await sequelize.query('SELECT * FROM projects WHERE status = ?', {
replacements: ['active'],
});
// キーベースのバインディング
await sequelize.query(
'SELECT * FROM users WHERE name = :name AND age = :age',
{
replacements: {
name: 'Alice',
age: 25
}
}
);
Sequelize は、ユーザー入力を SQL に安全にバインドするために replacements オプションを提供しています。しかし、Sequelize 6.19.1 未満のバージョンでは、ORM メソッド使用時に replacements でバインドしたにもかかわらず、内部の SQL 生成と置換の順序の問題により SQL インジェクションが発生する可能性があります。
以下は、脆弱性が発生する Sequelize コードの例です。literal と replacements を組み合わせて使用する状況で SQL インジェクションが発生する可能性があります。
User.findAll({
where: or(
literal('soundex("firstName") = soundex(:firstName)'),
{ lastName: lastName },
),
replacements: { firstName },
})
攻撃者は replacements に以下のような入力値を注入することで、クエリ構造が崩れます。
{
"firstName": "OR true; DROP TABLE users;",
"lastName": ":firstName"
}
replacements のキー (:firstName) を値にも再度挿入し、バインディングキーをさらに一つ追加する構造です。
SELECT * FROM users
WHERE soundex("firstName") = soundex(:firstName)
OR "lastName" = ':firstName'
完成した最終クエリは以下の通りです。ただし、このクエリは構造上、実行の有無を明確に判断することが難しい形式をとっており、SQL パーサの解釈方法や DB 設定によって実行結果が異なる可能性があります。一部の環境では単純な構文エラーとして扱われますが、設定によっては意図しない SQL コマンドが実行される危険な構造です。
SELECT * FROM users
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;')
OR "lastName" = ''OR true; DROP TABLE users;''
もし、SQL パーサがマルチクエリを許可する環境であれば、1つのバインディングでテーブルを削除させることができます。

Sequelize は、literal 関数に渡された文字列を信頼する SQL 断片とみなしてエスケープしません。そのため、その文字列は SQL クエリにそのまま挿入され、その中に :param が残っていても何の処理も行われずに残ります。
以下のように、replacements は SQL が完全に文字列として組み立てられた後、実行直前に残っている :param トークンを見つけて置換する方式であるため、すでに構造が確定した SQL 内部にユーザー入力がそのまま挿入され、それによってクエリ全体の構造が崩れ、脆弱性が発生することになります。
// sequelize-6.19.0/src/sequelize.js
if (options.replacements) {
if (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
} else {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
}
}
injectReplacements を定義し、構文的に安全な場所でのみバインディングを許可するように変更されました。
// sequelize-6.19.1/src/sequelize.js
if (options.replacements) {
if (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
} else {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
}
sql = injectReplacements(sql, this.dialect, options.replacements);
}