
Prova di concetto e analisi tecnica di CVE-2023-25813, una vulnerabilità di SQL injection in Sequelize ORM versioni precedenti alla 6.19.1, inclusi dettagli di sfruttamento e informazioni sulla patch.
replacements non viene correttamente escapato, il che può portare a SQL Injection in determinate configurazioni di query.Sequelize è una libreria ORM (Object-Relational Mapping) utilizzabile in Node.js. L'ORM automatizza la trasformazione tra oggetti e database relazionali, permettendo di interrogare, creare, modificare ed eliminare record del database tramite oggetti senza scrivere direttamente query SQL.
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
replacements è un meccanismo di binding in Sequelize per iniettare in modo sicuro i valori di input dell'utente nelle query SQL grezze o nei metodi ORM. Viene utilizzato come principale mezzo per prevenire SQL Injection.
// Binding posizionale
await sequelize.query('SELECT * FROM projects WHERE status = ?', {
replacements: ['active'],
});
// Binding basato su chiave
await sequelize.query(
'SELECT * FROM users WHERE name = :name AND age = :age',
{
replacements: {
name: 'Alice',
age: 25
}
}
);
Sequelize fornisce l'opzione replacements per associare in modo sicuro i valori di input dell'utente al SQL. Tuttavia, nelle versioni precedenti alla 6.19.1, quando si utilizzano metodi ORM, nonostante il binding tramite replacements, a causa di un problema nell'ordine di generazione e sostituzione del SQL interno può verificarsi SQL Injection.
Di seguito un esempio di codice Sequelize che mostra la vulnerabilità. Combinando literal e replacements, può verificarsi SQL Injection.
User.findAll({
where: or(
literal('soundex("firstName") = soundex(:firstName)'),
{ lastName: lastName },
),
replacements: { firstName },
})
L'attaccante può iniettare i seguenti valori in replacements, rompendo la struttura della query.
{
"firstName": "OR true; DROP TABLE users;",
"lastName": ":firstName"
}
Viene reinserita la chiave replacements (:firstName) anche nel valore, creando una struttura con una chiave di binding aggiuntiva.
SELECT * FROM users
WHERE soundex("firstName") = soundex(:firstName)
OR "lastName" = ':firstName'
La query finale completa è la seguente. Tuttavia, questa query è strutturalmente difficile da valutare chiaramente per l'esecuzione, e il risultato può variare a seconda del modo in cui il parser SQL interpreta la query e delle impostazioni del database. In alcuni ambienti viene gestita come un semplice errore di sintassi, ma a seconda della configurazione può rappresentare una struttura pericolosa in cui comandi SQL indesiderati possono essere eseguiti.
SELECT * FROM users
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;')
OR "lastName" = ''OR true; DROP TABLE users;''
Se l'ambiente consente query multiple nel parser SQL, una singola associazione può eliminare una tabella.

Sequelize considera le stringhe passate alla funzione literal come frammenti SQL fidati e non le escapa. Pertanto, la stringa viene inserita così com'è nella query SQL, e anche se contiene :param, viene lasciata senza alcuna elaborazione. Come mostrato di seguito, poiché replacements trova e sostituisce i token :param rimanenti solo dopo che il SQL è stato completamente assemblato in una stringa, subito prima dell'esecuzione, l'input dell'utente viene inserito così com'è nel SQL già strutturato, rompendo l'intera struttura della query e causando la vulnerabilità.
// 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);
}
}
È stata definita injectReplacements per consentire il binding solo in posizioni sintatticamente sicure.
// 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);
}