انتقال للمقال
وقت القراءة: ≈ 10 دقائق

كيف تحمي نفسك من ثغرة الـ SQL Injection

السلام عليكم ورحمة الله وبركاته

المقدمة

اليوم سنتحدث عن ثغرة مشهورة وهي الـ SQL Injection
سنشرح فكرة الثغرة وخطورتها مع أمثلة باستخدام Raw SQL
ثم امثلة باستخدام ORM و Query Builder في Laravel
لأنك قد تكون معرض لهذه الثغرة حتى وإن لم تكن تستخدم Raw SQL بشكل مباشر
لأنك حتى تستخدم ORM أو Query Builder بطريقة خاطئة تجعلك تقع في هذه الثغرة

وسنرى كيف يتم استغلال هذه الثغرة وكيف نحمي أنفسنا منها

هذه الثغرة موجودة منذ زمن طويل وتعد من أخطر الثغرات الأمنية
وأكثرها شيوعًا في تطبيقات الويب التي تتعامل مع قواعد البيانات
وسبب انتشارها هو الجهل بالثغرة نفسها وكيف تحدث وكيف يتم استغلالها وكيف نحمي أنفسنا منها

ما هى الـ SQL Injection ؟

الـ SQL Injection هى نوع من الهجمات يتم فيها استغلال البيانات التي تأتي من المستخدم
وإدخال أوامر SQL ضمن هذه البيانات بحيث يتم تنفيذ هذه الأوامر على قاعدة البيانات كأنها جزء من الـ Query الأصلية بدلاً من اعتبارها بيانات عادية

تخيل معي أن لديك كود بسيط يقوم بالبحث عن مستخدم في قاعدة البيانات بناءً على البريد الإلكتروني
وأنت تأخذ البريد الإلكتروني من المستخدم وتضعه مباشرة في الـ Query بهذا الشكل:

SELECT * FROM users WHERE email = 'user_input_email';

أين المشكلة هنا ؟

هنا الخانة التي نستقبلها من المستخدم هي user_input_email
وهذه القيمة تخزن في متغير في الكود الخاص بنا ثم تمرر في الـ Query بهذا الشكل:

$email = $request->input('email');

$query = "SELECT * FROM users WHERE email = '$email'";
$results = DB::select($query);

هناك نستقبل الـ email من الـ Request
ثم نضعه مباشرة داخل الـ Query ثم ننفذ الـ Query على قاعدة البيانات

هذا الكود البسيط الآن معرض لخطر الـ SQL Injection

لأنك بكل بساطة تأخذ البيانات من المستخدم وتضعها داخل الـ Query بشكل مباشر وهذا هو جوهر المشكلة

هنا نسأل سؤال ماذا لو قام المستخدم بإدخال أوامر SQL بدلاً من البريد الإلكتروني ؟
هنا تبدأ المشكلة الحقيقية

لأنك طالما أنك تأخذ البيانات من المستخدم وتضعها داخل الـ Query بشكل مباشر
فأنت تفتح باب لأي شخص لاستغلال ثغرة الـ SQL Injection بسهىلة
ويستطيع سرقة البيانات أو حذفها أو تعديلها أو أي شيء يريده

شرح طريقة استغلال الـ SQL Injection بشكل عملي

لنفترض أن لدينا دالة بسيطة في Laravel تقوم بالبحث عن مستخدم بناءً على البريد الإلكتروني كما ذكرنا سابقًا
والكود كان كالتالي:

public function getUserByEmail(Request $request)
{
    $email = $request->input('email');

    $user = DB::selectOne("SELECT * FROM users WHERE email = '$email'");

    return $user;
}

لاحظ أننا نأخذ الـ email من الـ Request ونضعه مباشرة في الـ Query
ثم ننفذ الـ Query باستخدام DB::selectOne() التي تنفذ الـ Raw SQL وترجع أول نتيجة

هنا فتحنا الباب أمام ثغرة الـ SQL Injection لكن سؤال كيف يتم استغلال الثغرة ؟

لنفترض أن المستخدم العادي أدخل بريده الإلكتروني الجميل eltabaraniahmed@gmail.com
بالتالي ستصبح الـ Query كالتالي:

SELECT * FROM users WHERE email = 'eltabaraniahmed@gmail.com'

هنا ليس هناك أي مشكلة، القاعدة ستبحث عن المستخدم بهذا البريد وترجع النتيجة
حسنًا، لنفترض الآن أن المستخدم قام بوضع ' في خانة البريد الإلكتروني
بالتالي ستصبح الـ Query كالتالي:

SELECT * FROM users WHERE email = '''

وسيقوم الـ SQL بإرجاع Exception لأن هناك مشكلة في بناء الجملة

SQLSTATE[42000]: Syntax error or access violation: 1064
You have an error in your SQL syntax

عندما يرى المهاجم هذا الخطأ، سيبتسم ابتسامة عريضة تصل إلى أذنيه
لأنه قد وجد ضالته الجميلة وهى ثغرة الـ SQL Injection
طالما الـ Exception أتى من الـ SQL بشكل مباشر بالتالي الـ Query وصلت إلى قاعدة البيانات كما هي

الآن يستطيع المهاجم استغلال هذه الثغرة بكل سهولة عن طريق إدخال أوامر SQL في خانة البريد الإلكتروني
لنفترض أن المهاجم أدخل القيمة التالية في خانة البريد الإلكتروني ' OR '1'='1
لنرى كيف ستصبح الـ Query:

SELECT * FROM users WHERE email = '' OR '1'='1'

لاحظ كيف تغيرت الـ Query بشكل كامل من WHERE email = '$email' إلى WHERE email = '' OR '1'='1'
بسبب أن قيمة الـ email أصبحت ' OR '1'='1

SELECT * FROM users WHERE email = '$email'
                                      |
 $email = ' OR '1'='1                 |
                                      V
SELECT * FROM users WHERE email = '' OR '1'='1'

لقد حاولت توضيح ماذا حدث من خلال هذا الرسم التوضيحي البسيط
لاحظ أن قيمة الـ email احتوت على ' في البداية والتي أغلقت الـ String المفتوحة في الـ Query
ثم تم إضافة OR بالتالي طالما هناك OR فأي شرط AND في الـ Query سيتم تجاهله
ثم في النهاية '1'='1 وهو شرط يعطي قيمة true دائمًا بالتالي كأنه كاتب OR true والتي تجعل الـ WHERE دائمًا بـ true

لاحظ أن '1'='1 لا يملك ' في نهايته لأن نهاية الـ Query الأصلية تحتوى على ' في النهاية

على أي حال، ماذا تعني هذه الـ Query الجديدة ؟
تعني ببساطة إرجاع كل المستخدمين في قاعدة البيانات
لكن بسبب أننا استخدمنا DB::selectOne() فسيتم إرجاع أول مستخدم فقط من قاعدة البيانات


قد تقول لي الآن حسنًا، هذا ليس خطيرًا جدًا، فقط سيتم إرجاع أول مستخدم على أي حال
الأمر ليس بهذه البساطة، المهاجم يمكنه استغلال هذه الثغرة بطرق أكثر خطورة
هو يستطيع الآن أدخال أي أمر SQL يريده داخل خانة البريد الإلكتروني
لأنه يستطيع إغلاق الـ String المفتوحة في الـ Query ثم إضافة أي أمر SQL يريده
يستطيع سرقة كل البيانات في كل الجداول، يستطيع حذف الجداول، يستطيع تعديل البيانات، يستطيع حتى تسجيل الدخول بدون كلمة مرور

لنأخذ على سبيل المثال سيناريو حذف جدول المستخدمين بالكامل

ماذا لو أدخل المهاجم القيمة التالية ؟

'; DROP TABLE users; --

ستصبح الـ Query كالتالي:

SELECT * FROM users WHERE email = ''; DROP TABLE users; --'

هنا المهاجم:

  1. أغلق الـ Query الأولى بـ ';
  2. أضاف أمر جديد DROP TABLE users; لحذف جدول المستخدمين بالكامل
  3. أضاف -- لتحويل باقي الـ Query إلى تعليق حتى يتم تجاهل أي أوامر لاحقة

وهكذا تم حذف جدول المستخدمين بالكامل من قاعدة البيانات
وبالتالي تم طردك من وظيفتك بسبب إهمالك في تأمين التطبيق ولأنك لم تستطع حماية نفسك من ثغرة الـ SQL Injection

لكن لا تقلق أكمل معي قراءة هذه المقالة المتواضعة والجميلة وسأريك كيف تحمي نفسك من هذه الثغرة بكل سهولة


طبعًا معظم من بيستغلوا هذه الثغرات لا يقومون بحذف أي شيء
بل يقومون بسرقة البيانات لكي يستطيعون بيعها إلى المنافسين أو استخدامها في هجمات أخرى من أجل ربح المال لا أكثر
ويستخدمون أدوات وبرامج تساعدهم في استغلال هذه الثغرات بشكل تلقائي
بمجرد ما يتم شم رائحة وجود ثغرة الـ SQL Injection في التطبيق
فكل شيء يكون بين أيديهم بكل سهولة

كيف نحمي تطبيقاتنا من الـ SQL Injection ؟

الآن بعد أن فهمنا خطورة هذه الثغرة، دعنا نتعرف على طرق الحماية منها
ولحسن الحظ بسبب شيوع وشهرة هذه الثغرة، هناك عدة طرق فعالة لحمايتنا منها
ومعظم المكاتب والـ ORM توفر لنا طرق سهلة وآمنة للتعامل مع قواعد البيانات بدون الوقوع في هذه الثغرة

الطريقة الأولى: استخدام Prepared Statements

الـ Prepared Statements تسمى أحيانًا بـ Binding أو Parameter Binding أو Parameterized Queries
هي طريقة آمنة لتنفيذ الـ Query بحيث يتم فصل الـ Query عن البيانات التي تأتي من المستخدم

public function getUserByEmail(Request $request)
{
    $email = $request->input('email');

    // Parameterized Query
    $user = DB::selectOne("SELECT * FROM users WHERE email = ?", [$email]);

    return $user;
}

لاحظ الفرق هنا، بدلاً من وضع $email مباشرة داخل الـ Query
قمنا بوضع علامة ? كـ placeholder ثم مررنا القيمة بشكل منفصل
والمكتبة أو الـ ORM تتولى مهمة ربط القيمة بالـ placeholder بشكل آمن
بحيث أنها تعامل مع القيمة كـ بيانات فقط وليس كأمر SQL

بالتالي حتى لو أدخل المهاجم ' OR '1'='1 فإن قاعدة البيانات ستبحث حرفيًا عن مستخدم بالبريد الإلكتروني ' OR '1'='1
ولن تجده بالتالي سيرجع لك نتيجة فارغة
هكذا تكون قد حميت نفسك من ثغرة الـ SQL Injection بكل سهولة

بعض المكاتب تدعم فكرة الـ Named Parameters بدلاً من ?:

$users = DB::select("SELECT * FROM users WHERE email = :email", ['email' => $email]);

وهى نفس الفكرة ولكن باستخدام أسماء للـ placeholders بدلاً من ?

الطريقة الثانية: استخدام Query Builder

أي ORM سيوفر لك Query Builder والذي يبني الـ Queries بشكل آمن تلقائيًا بدون الحاجة للقلق بشأن الـ SQL Injection
لأن Query Builder مصمم ليكون abstract بحيث لا تحتاج لكتابة أي Raw SQL بنفسك
ويقوم هو داخليًا بالتخلص من أي ثغرات سواء كانت الـ SQL Injection أو غيرها
ويقوم بعمل Optimization للـ Queries أيضًا والكثير من المزايا الأخرى

وLaravel يوفر Query Builder قوي وسهل الاستخدام

public function getUserByEmail(Request $request)
{
    $email = $request->input('email');

    // Query Builder
    $user = DB::table('users')->where('email', $email)->first();
    // Or using Eloquent ORM
    // $user = User::where('email', $email)->first();

    return $user;
}

هنا الـ Query Builder يتولى مهمة بناء الـ Query بشكل آمن ويحولها إلى Prepared Statement تلقائيًا
وبالتالي لا داعي للقلق بشأن ثغرة الـ SQL Injection

وأي ORM في أي لغة برمجة أو Framework سيوفر لك هذه الميزات بسهولة

الاستخدام الخاطيء للـ ORM والـ Query Builder

لا تظن أنك بمجرد استخدامك لـ ORM أو Query Builder فأنت محمي تمامًا من ثغرة الـ SQL Injection
لأنك قد تستخدم هذه الأدوات بشكل خاطئ وتفتح لنفسك باب الثغرة من جديد برغم وجود هذه الأدوات

بسبب أن بعض الدوال في ORM أو Query Builder تسمح لك بكتابة Raw SQL
وإذا استخدمتها بشكل خاطئ فقد تقع في فخ ثغرة الـ SQL Injection مرة أخرى

فعلى سبيل المثال في Laravel هناك دوال مثل DB::select() و DB::statement() و whereRaw() تسمح لك بكتابة Raw SQL
وهذه الدوال معرضة لخطر الـ SQL Injection إذا استخدمتها بشكل خاطئ

$users_1 = DB::select("SELECT * FROM users WHERE email = '$email'");
$users_2 = DB::statement("UPDATE users SET name = '$name' WHERE id = $id");
$users_3 = User::whereRaw("email = '$email'")->get();

كل هذه الأمثلة معرضة لخطر الـ SQL Injection
لأننا نأخذ البيانات من المستخدم ونضعها مباشرة في الـ Query
لكن لا تقلق فكلها توفر لك Prepared Statements أو Parameter Binding لتجنب هذه الثغرة

$users_1 = DB::select("SELECT * FROM users WHERE email = ?", [$email]);
$users_2 = DB::statement("UPDATE users SET name = ? WHERE id = ?", [$name, $id]);
$users_3 = User::whereRaw("email = ?", [$email])->get();

لماذا أستخدم Raw SQL طالما هناك ORM و Query Builder ؟

هناك حالات قد تحتاج فيها لاستخدام Raw SQL
مثلًا إذا كنت تحتاج إلى تنفيذ Query معقدة جدًا لا يمكن تحقيقها بسهولة باستخدام ORM أو Query Builder
فعلى سبيل المثال CASE Statements أو Subqueries أو Joins معقدة
أو بعض الأوامر ومميزات الـ SQL التي لا يدعمها الـ ORM أو الـ Query Builder بشكل مباشر
هنا قد تضطر لاستخدام Raw SQL لكن علينا أن نكون حذرين جدًا في كيفية استخدامنا لها
ونستخدم دائمًا Prepared Statements أو Parameter Binding لتجنب ثغرة الـ SQL Injection

والأمر الأخر هو استخدام Raw SQL يكون أفضل من حيث سرعة تنفيذ الـ Query
في بعض الحالات المعقدة جدًا التي لا يستطيع الـ ORM أو الـ Query Builder تحقيقها بكفاءة

على أي حال لو وجدت نفسك تستخدم Raw SQL فعليك أن تستخدم الـ Prepared Statements دائمًا

ملخص

ملخص ما تعلمناه اليوم هو أن تتعامل مع المستخدم على أنه شخص شرير يريد سرقة بياناتك أو تخريبها ويتسبب بطردك من وظيفتك

قد أكون بالغت قليلًا في الوصف لكن هذا هو الواقع
لا تستأمن أي بيانات قادمة من المستخدم مهما كان مصدرها
ولا تثق بالـ Validation الذي يقوم بها صديقك الـ Frontend
لأنها يمكن أن يتم تجاوزها بسهولة كأنها غير موجودة

على أي حال، تعلمنا اليوم ما هي ثغرة الـ SQL Injection وكيف تحدث وكيف يتم استغلالها بشكل عملي
ورأينا أمثلة باستخدام Raw SQL في Laravel
ثم رأينا كيف نحمي أنفسنا من هذه الثغرة باستخدام Prepared Statements و Query Builder و Eloquent ORM
وتعلمنا أن استخدام ORM أو Query Builder لا يعني بالضرورة أنك محمي من الثغرة إذا استخدمتها بشكل خاطئ
بحيث أنك قد تستخدم دوال تسمح بكتابة Raw SQL وتضع فيها البيانات التي حصلت عليها من المستخدم بشكل مباشر
لذلك علينا دائمًا أن نكون حذرين في كيفية تعاملنا مع البيانات التي تأتي من المستخدم
سواء من الـ Body أو الـ Query Parameters أو الـ Headers أو أي مكان آخر

ونستخدم دائمًا Prepared Statements أو Parameter Binding لتجنب هذه الثغرة في حالة استخدامنا لـ Raw SQL