
ثغرة حقن SQL في Django
Django هو إطار تطبيق ويب مفتوح المصدر، مكتوب بلغة Python، مبني على نموذج MVC (Model - View - Controller). تم بناؤه في البداية لإدارة مواقع الويب لمحتوى الأخبار المملوكة لمجموعة النشر Lawrence، وهو نظام إدارة محتوى (CMS).
توجد ثغرة SQL injection في Django الإصدارات 3.1.x -> 3.1.13 والإصدارات 3.2.x -> 3.2.5.
سبب هذه الثغرة هو أن وظيفة تصفية البيانات المدخلة التي يتحكم فيها المستخدم في QuerySet.order_by() غير كافية لمنع هجمات SQL injection. يمكن استغلال هذه الثغرة للسماح للمهاجم بتنفيذ إجراءات غير مصرح بها تؤدي إلى تسريب بيانات حساسة.
| CVE - ID | CVE-2021-35042 |
|---|---|
| Severity | 9.8 - CRITICAL |
| CWE - ID | CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| تاريخ نشر الثغرة | 1/7/2021 |
| البرامج المتأثرة | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| مطلوب مصادقة | غير مطلوب |
في Django، يتم إنشاء الجداول وتعريف الحقول في قاعدة البيانات عن طريق تعريف فئة نموذج (model) في ملف models.py. في هذا المثال، نعلن عن جدول باسم Wolf وحقل باسم name.


يُستخدم إطار عمل ORM المدمج في Django للتعامل مع قاعدة البيانات، ونتيجة الاستعلام هي مجموعة تسمى QuerySet.
order_by(fields)
افتراضيًا، ترجع order_by() QuerySet مرتبة حسب الترتيب المحدد في خيار ordering في Meta الخاص بالنموذج. يمكننا تجاوز شرط order_by في كل استعلام باستخدام الطريقة order_by().
مثال
wolves = Wolf.objects.order_by('-name', 'id')
سيتم ترتيب نتيجة الاستعلام أعلاه تنازليًا حسب الحقل name، ثم تصاعديًا حسب id. تشير علامة السالب قبل اسم الحقل name إلى ترتيب النتيجة تنازليًا.
المثال التالي سيرتب النتيجة حسب الحقل المستلم من المستخدم، وإذا لم يتم تمرير أي قيمة فسيتم الترتيب حسب الحقل
id.
النتيجة

في الإصدارين 3.1 و 3.2، يسمح Django بدمج طريقة الاستعلام مع اسم الجدول في استعلام order_by. وهذا هو السبب الرئيسي لهذه الثغرة.
تمرير اسم جدول يعطينا نفس النتيجة كتمرير اسم الحقل العادي
cve202135042_wolf هو اسم الجدول
أولاً، يستدعي التطبيق مباشرة دالة order_by()، ويتم تعريف الكود الذي يعالج دالة order_by() في:
django/db/models/query.py

تقوم دالة order_by() بأمرين
- تحذف جميع الترتيبات الحالية التي يتم استدعاؤها بواسطة order_by() وتحذف المعامل الافتراضي الذي يمرر عندما يستقبل order_by قيمة مختلفة.
- تمرر المعاملات إلى order_by. تقوم الدالة
add_ordering()بهذا الأمر.
def add_ordering(self, *ordering):
"""
Add items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or OrderBy
expressions.
If 'ordering' is empty, clear all ordering from the query.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item,
category=RemovedInDjango40Warning,
stacklevel=3,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if self.extra and item in self.extra:
continue
# names_to_path() validates the lookup. A descriptive
# FieldError will be raise if it's not.
self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
elif not hasattr(item, 'resolve_expression'):
errors.append(item)
if getattr(item, 'contains_aggregate', False):
raise FieldError(
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s' % item
)
if errors:
raise FieldError('Invalid order_by arguments: %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
المعامل الذي يُمرر إلى add_ordering() هو مصفوفة.
مثال عندما يُمرر المعامل كالتالي:
wolves = Wolf.objects.order_by( 'name' , 'id' )عندها، سيقوم التطبيق بتحويله إلى استعلام في قاعدة البيانات كالتالي:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
عند تمرير القيم، ستقوم دالة add_ordering بفحص كل عنصر في المصفوفة، إذا كان string، فسيتم فحصه في 5 حالات:
if '.' in item:تتحقق مما إذا كان استعلامًا يحتوي على اسم عمود مع اسم جدول محدد في جملة SQL. إذا كان كذلك، فسيصدر تحذيرًا ويستمرcontinue.if item == '?':إذا كانت قيمة العنصر هي علامة '?'، فسيتم ترتيب النتيجة عشوائيًا،continue.if item.startswith('-'):إذا بدأ العنصر بالحرف '-', فسيتم ترتيب نتيجة الاستعلام تنازليًا (DESC).if item in self.annotations:يتحقق مما إذا كان يحتوي على تعليق (comment)، إذا كان كذلك،continue.if self.extra and item in self.extra:يحدد ما إذا كان هناك إضافات (extra) وإذا كان كذلك،continue.
بعد 5 فحوصات، يستمر تمرير المعامل إلى الدالة self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) للتحقق مما إذا كان اسم عمود صالحًا، ثم إذا كان صالحًا، سيتم إضافته إلى self.ordering في الفئة Query لمعالجته لاحقًا.
يقوم ORM الخاص بـ Django بتصفية البيانات المدخلة في الاستعلام بشكل صارم، لكن التغيير في الكود المصدري هذه المرة أدى إلى SQL injection بسبب افتراض المؤلف أن اسم العمود إذا كان UUID (Universal Unique Identifier) فلن يتمكن استعلام order_by من التنفيذ.
بمعنى أنه إذا كانت البيانات المدخلة هي xxx-xxx-xxx-xxx (تنسيق UUID)، فلن يتمكن الاستعلام من التنفيذ.
الكود قبل التغيير
# django/db/models/sql/constants.py
ORDER_PATTERN = _lazy_re_compile ( r '\?|[-+]?[.\w]+$' )