CodeAuditAgent
كل المقالات
  • الأمان
  • SQL
  • OWASP

منع حقن SQL في Node.js وPython وGo

مقتطفات SQL مصابة ومُصلحة لـ pg وPrisma وpsycopg وSQLAlchemy وdatabase/sql في Go، مع ORDER BY ديناميكي وقوائم IN آمنة. مربوطة بـ CWE-89.

· قراءة في 7 دقائق · Lina Source LLC

حقن SQL (CWE-89) من أقدم الأخطاء على الويب وما يزال من أشدها ضررًا. سببه لم يتغير قط: تُلصق مدخلات المستخدم داخل نص الاستعلام، فلا تستطيع قاعدة البيانات التمييز بين البيانات والكود. والإصلاح لم يتغير أيضًا: أرسل الاستعلام والقيم كلًّا على حدة، ودع المشغّل (driver) يربطها.

ما تغيّر هو مكان وجود الخلل. تستخدم معظم الفرق أداة ORM للاستعلامات اليومية، لذا صار الحقن يظهر في منافذ الهروب: الاستعلام الخام المكتوب لتقرير ما، ونقطة نهاية البحث ذات الترتيب الديناميكي، وسكربت الترحيل، وأداة الإدارة. يستعرض هذا الدليل النسخة المصابة والمُصلحة في ثلاث بيئات، ثم يتناول الحالتين اللتين لا تحلهما المعاملات وحدها.

نادرًا ما يقتصر الأثر على جدول واحد. فالاستعلام الواحد القابل للحقن يعمل عادةً بصلاحيات التطبيق الكاملة على قاعدة البيانات، فيستطيع قراءة بيانات كل العملاء، بما فيها تجزئات كلمات المرور ورموز API، وتعديل الصفوف، بل والوصول في بعض قواعد البيانات إلى نظام الملفات أو إلى خوادم أخرى. وتستخرج التقنيات العمياء البيانات بتًا بعد بت حتى لو لم تُعرض نتيجة الاستعلام على المستخدم أبدًا، لذا تظل نقطة النهاية التي تعيد true أو false فقط قابلة للاستغلال.

لماذا تنجح المعاملات ويفشل التهريب

في الاستعلام المُعلَّم، يرسل المشغّل نص SQL مع عناصر نائبة، وتنتقل القيم كبيانات منفصلة. تحلّل قاعدة البيانات العبارة قبل أن ترى القيمة أصلًا، فتصبح علامة الاقتباس أو الفاصلة المنقوطة في المدخلات مجرد حرف داخل سلسلة نصية. أما التهريب اليدوي فيحاول جعل القيمة آمنة للصقها في نص SQL، ويفشل مع الترميزات والسياقات الرقمية والمعرّفات وكل حالة حدّية لم تخطر ببالك. لا تهرّب؛ بل اربط.

Node.js: pg وPrisma

مع node-postgres، النمط المصاب هو template literal يُمرَّر إلى query. والإصلاح عنصر نائب $1 ومصفوفة قيم. أما Prisma فآمنة افتراضيًا، لكن لديها واجهتان خامتان، وواحدة فقط منهما آمنة مع الاستيفاء.

@@CODE@@

فخ دقيق في Prisma: بناء سلسلة الاستعلام أولًا ثم تمريرها إلى $queryRaw يُفقدك الحماية. فالأمان مصدره صياغة tagged template. إذا احتجت إلى تركيب أجزاء، فاستخدم Prisma.sql وPrisma.join، اللتين تُبقيان القيم كمعاملات. والقاعدة نفسها تنطبق على مكتبات tagged template الأخرى مثل postgres.js وslonik: الوسم هو ما يجعل الاستيفاء آمنًا، لذا فإن سلسلة استعلام مُجمّعة مسبقًا وممرَّرة كقيمة عادية تتجاوزه.

Python: psycopg وSQLAlchemy

في Python، الأدوات الخطرة هي f-strings والعامل % وstr.format حين تُطبَّق على SQL. تستخدم psycopg العناصر النائبة %s، التي تشبه تنسيق السلاسل لكنها ليست كذلك: القيم تذهب في الوسيط الثاني لـ execute، ولا تمر أبدًا عبر العامل %. وبنية text() في SQLAlchemy آمنة حين تستخدم معاملات ربط مسمّاة، وغير آمنة حين تنسّق القيم داخل السلسلة.

تفصيل واحد في psycopg يوقع الناس: العنصر النائب هو %s (أو %(name)s للقيم المسمّاة) أيًّا كان نوع العمود، ولا تضع حوله علامات اقتباس أبدًا. كتابة '%s' بعلامات اقتباس تعيد المعامل جزءًا من سلسلة حرفية وتُفسد الاستعلام. والأمر نفسه مع المعاملات المسمّاة في SQLAlchemy: :status وليس ':status'.

@@CODE@@

Go: database/sql

تدعم database/sql في Go العناصر النائبة أصلًا، لكن صياغتها تعتمد على المشغّل: $1 لمشغّلات PostgreSQL مثل pgx، و? لـ MySQL وSQLite. النمط المصاب هو استخدام fmt.Sprintf أو تجميع السلاسل لبناء عبارة WHERE. ولأن Go لغة ذات أنواع ثابتة، يُغري الافتراض بأن معاملًا من نوع int لا يمكن أن يكون خطرًا. هذا صحيح بالنسبة للقيمة نفسها، لكن عادة بناء الاستعلامات بـ Sprintf تمتد إلى المعاملات النصية المجاورة لها، لذا أبقِ كل استعلام على العناصر النائبة.

@@CODE@@

منافذ الهروب في أدوات ORM

تحمي أدوات ORM باني الاستعلامات، لا كل دالة في الأداة. هذه هي الأماكن التي تبحث فيها أولًا في أي قاعدة كود:

  • Prisma: الدالتان $queryRawUnsafe و$executeRawUnsafe، وأي استدعاء لـ $queryRaw يتلقى سلسلة مبنية مسبقًا بدلًا من tagged template.
  • Sequelize وTypeORM: الدالة sequelize.query، واستدعاءات .where() في باني الاستعلامات حين تُعطى سلسلة مُجمّعة، وعبارات order أو group الخام.
  • Knex: الدالتان knex.raw وwhereRaw مع قيم مُستوفاة بدلًا من روابط ?.
  • SQLAlchemy: الدالة text() مع f-strings، والدالة literal_column() أو أسماء أعمدة مأخوذة من المدخلات.
  • Django: الدوال .raw() و.extra() وcursor.execute مع سلاسل مُنسّقة.
  • GORM: الدوال Where وOrder وRaw حين تُستدعى بمخرجات fmt.Sprintf بدلًا من الوسائط.

ORDER BY الديناميكي: المعاملات لا تنفع هنا

العناصر النائبة تربط القيم، لا المعرّفات. لا يمكنك كتابة ORDER BY $1 وتمرير اسم عمود؛ إذ ستفرز قاعدة البيانات حسب قيمة ثابتة. لذا يميل الجدول القابل للفرز إلى دفع المطورين للعودة إلى بناء السلاسل، ومن هناك يعود الحقن. والقيد نفسه ينطبق على أسماء الجداول وأسماء المخططات وكلمات SQL المفتاحية مثل ASC وDESC.

النمط الآمن هو قائمة سماح تربط مفاتيح الفرز الظاهرة للمستخدم بأسماء أعمدة معروفة. المدخلات تختار مُدخلًا من القائمة؛ ولا تصبح SQL أبدًا. ويُعامل اتجاه الفرز بالطريقة نفسها: يُربط بقيمة ثابتة هي ASC أو DESC.

@@CODE@@

إذا احتجت فعلًا إلى معرّف ديناميكي لا يمكن وضعه في قائمة سماح، فاستخدم آلية اقتباس المعرّفات في المشغّل بدلًا من آليتك الخاصة. في psycopg هي sql.SQL(...).format(sql.Identifier(name))؛ وفي pgx هي pgx.Identifier{name}.Sanitize(). وتبقى قائمة السماح أفضل، لأن الاقتباس يجعل الاسم آمنًا لكنه لا يجعله بالضرورة صالحًا أو مسموحًا به. فالمعرّف المقتبس ما يزال يتيح للمستدعي الفرز أو الترشيح حسب أي عمود في الجدول، بما في ذلك أعمدة لم تقصد كشفها قط، مثل تجزئة كلمة المرور أو درجة داخلية. والفرز حسب عمود مخفي قد يسرّب قيمه عبر ترتيب النتائج، مقارنةً تلو الأخرى.

قوائم IN دون بناء السلاسل

الترشيح حسب قائمة من المعرّفات هو الذريعة الشائعة الأخرى للتجميع. ولكل حزمة تقنية رئيسية طريقة آمنة لذلك:

  • PostgreSQL مع pg أو pgx: مرّر القائمة كاملة كمعامل مصفوفة واحد واكتب WHERE id = ANY($1). يرسلها المشغّل كمصفوفة Postgres.
  • Prisma: التعبير Prisma.sql`SELECT * FROM users WHERE id IN (${Prisma.join(ids)})` يتوسع إلى عنصر نائب لكل عنصر.
  • SQLAlchemy: استخدم text("... WHERE id IN :ids").bindparams(bindparam("ids", expanding=True))، ثم مرّر قائمة.
  • Go مع MySQL: ولّد سلسلة العناصر النائبة من طول القائمة (strings.Repeat("?,", n) بعد حذف الفاصلة الأخيرة) ومرّر القيم كوسائط. تُولَّد العناصر النائبة فقط، ولا تُولَّد القيم أبدًا.
  • ارفض القوائم الفارغة قبل تشغيل الاستعلام، وضع حدًا أقصى لحجم القائمة حتى لا تُستخدم نقطة النهاية لبناء عبارات ضخمة.

الدفاع المتعدد الطبقات

الاستعلامات المُعلَّمة هي الإصلاح. أما ما يلي فيقلّص نطاق الضرر حين يفلت استعلام ما:

  • اتصل بدور في قاعدة البيانات لا يملك إلا الصلاحيات التي يحتاجها التطبيق. نادرًا ما يحتاج تطبيق الويب إلى DROP أو ALTER أو الوصول إلى مخططات أخرى.
  • لا تُعِد أخطاء قاعدة البيانات الخام إلى العملاء. فالحقن القائم على الأخطاء يعتمد على رؤيتها.
  • تحقق من الأنواع عند الحدود: المعرّف الذي يُفترض أن يكون عددًا صحيحًا أو UUID يجب تحليله على هذا الأساس قبل أن يصل إلى طبقة البيانات.
  • اضبط مهلة زمنية للعبارات، فهي تحدّ من الحقن الأعمى القائم على الوقت ومن الاستعلامات الجامحة على حد سواء.

العثور على ما لديك بالفعل

ابدأ بالبحث عن منافذ الهروب المذكورة أعلاه، ثم عن كلمات SQL المفتاحية قرب تنسيق السلاسل: SELECT أو WHERE داخل template literals أو f-strings أو Sprintf أو التجميع بـ +. كل نتيجة إما مربوطة بشكل صحيح، أو مبنية من قائمة سماح، أو خلل. لا تتجاوز مسارات الكود التي تبدو داخلية: مستوردات CSV ومهام cron ولوحات الإدارة كثيرًا ما تأخذ مدخلات جاءت في الأصل من مستخدم، على بُعد خطوة واحدة فقط. والقيم المقروءة من قاعدة بياناتك نفسها قد تحمل حمولة خُزّنت بأمان في وقت سابق ثم لُصقت في استعلام لاحقًا، وهو ما يُعرف بالحقن من الدرجة الثانية. يُجري CodeAuditAgent هذا التتبع عبر الملفات في مستودعات GitHub العامة والكود الملصوق، ويبلّغ عن اكتشافات CWE-89 مع الاستعلام المقتبس وكيفية وصول المدخلات إليه ونسخة مُصحّحة.

القاعدة التي تخرج بها قصيرة: القيم تذهب في المعاملات، والمعرّفات تأتي من قائمة سماح، ولا شيء من الطلب يُلصق في نص SQL أبدًا.