البرمجة مبتدئ 8 دقيقة

كيفية منع حقن SQL في PHP

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

الخطوات

  1. 1

    افهم الثغرة أولاً

    يحدث الحقن الكلاسيكي عند بناء الاستعلام عبر دمج السلاسل النصية:

    php
    // ثغرة — لا تفعل هذا أبداً
    $email = $_POST['email'];
    $result = mysqli_query($conn, "SELECT * FROM users WHERE email = '$email'");
    
    // يُرسل المهاجم: ' OR '1'='1
    // يصبح الاستعلام:
    // SELECT * FROM users WHERE email = '' OR '1'='1'
    // هذا يُعيد كل صف في الجدول.
  2. 2

    استخدم Prepared Statements مع PDO

    تُفصل Prepared Statements بنية الاستعلام عن البيانات. يتولى driver قاعدة البيانات الهروب داخلياً — البيانات لا يمكن أبداً تفسيرها كصياغة SQL.

    php
    // الاتصال بـ PDO
    $pdo = new PDO(
        'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
        'db_user',
        'db_password',
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    
    // آمن: placeholder بالموضع
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
    $stmt->execute([$email]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
  3. 3

    استخدم Named Placeholders لقراءة أفضل

    Named Placeholders أسهل في القراءة والصيانة عندما يحتوي الاستعلام على عدة معاملات، وتتجنب أخطاء الترتيب.

    php
    $stmt = $pdo->prepare(
        'INSERT INTO users (name, email, role) VALUES (:name, :email, :role)'
    );
    $stmt->execute([
        ':name'  => $name,
        ':email' => $email,
        ':role'  => $role,
    ]);
  4. 4

    توقف عن استخدام دوال الهروب — إنها التجريد الخاطئ

    mysql_real_escape_string (حُذفت في PHP 7) وaddslashes وmysqli_real_escape_string دوال هروب، ليست parameterization. يمكن تجاوزها بتشفيرات أحرف معينة، ولا تحمي من الحقن في السياقات الرقمية التي لا تُستخدم فيها الاقتباسات. Prepared Statements ليست "الشيء ذاته لكن بعمل أكثر" — إنها آلية مختلفة جوهرياً تُزيل سطح الهجوم بالكامل.

    php
    // خاطئ أيضاً — لا تفعل هذا
    $safeEmail = mysqli_real_escape_string($conn, $_POST['email']);
    $result = mysqli_query($conn, "SELECT * FROM users WHERE email = '$safeEmail'");
    
    // خاطئ للأعمدة الرقمية — لا اقتباسات، فالهروب لا يفعل شيئاً
    $id = mysqli_real_escape_string($conn, $_GET['id']);
    $result = mysqli_query($conn, "SELECT * FROM users WHERE id = $id");
    // المهاجم يُرسل: 1 UNION SELECT password FROM admins
  5. 5

    استخدم القائمة البيضاء للمُعرِّفات الديناميكية

    لا يمكن تمرير أسماء الجداول والأعمدة واتجاهات الترتيب كمعاملات PDO — سيضعها الـ driver بين اقتباسات كسلاسل نصية حرفية، مما يُفسد الاستعلام. بدلاً من ذلك، تحقق منها مقابل قائمة بيضاء صريحة قبل إدراجها.

    php
    // عمود ORDER BY ديناميكي يختاره المستخدم
    $allowedColumns = ['name', 'created_at', 'email'];
    $column = in_array($_GET['sort'] ?? '', $allowedColumns, true)
        ? $_GET['sort']
        : 'created_at'; // قيمة افتراضية آمنة
    
    $allowedDirections = ['ASC', 'DESC'];
    $direction = in_array(strtoupper($_GET['dir'] ?? ''), $allowedDirections, true)
        ? strtoupper($_GET['dir'])
        : 'DESC';
    
    // آمن للإدراج لأن القيم المعروفة الجيدة فقط تصل هنا
    $stmt = $pdo->prepare("SELECT * FROM users ORDER BY {$column} {$direction}");
    $stmt->execute();
  6. 6

    استخدم Query Builder أو Eloquent في Laravel بأمان

    يستخدم Query Builder وEloquent في Laravel داخلياً PDO Prepared Statements لجميع الأساليب القياسية. تحصل على حماية من الحقن تلقائياً.

    php
    // آمن — Query Builder يُضيف المعاملات تلقائياً
    $user = DB::table('users')->where('email', $email)->first();
    
    // آمن — Eloquent يفعل الشيء ذاته
    $user = User::where('email', $email)->first();
    
    // آمن أيضاً — ربط صريح على جزء raw
    $users = DB::table('users')
        ->whereRaw('email = ? AND active = ?', [$email, 1])
        ->get();
  7. 7

    تحديد وإصلاح الاستعلامات الخام غير الآمنة في Laravel

    تقبل DB::raw وselectRaw وwhereRaw وorderByRaw سلاسل SQL اعتباطية. إنها آمنة عند تمرير مدخل المستخدم كـ binding — لكنها تُعيد فتح ثغرة الحقن عند الدمج.

    php
    // ثغرة — دمج سلاسل نصية داخل استدعاء raw
    $users = DB::select("SELECT * FROM users WHERE name = '{$name}'");
    
    // آمن — تمرير المدخل كـ binding، ليس داخل السلسلة
    $users = DB::select('SELECT * FROM users WHERE name = ?', [$name]);
    
    // ثغرة — raw مع مدخل مدموج
    $users = DB::table('users')
        ->whereRaw("name = '{$name}'")->get();
    
    // آمن — raw مع placeholder للـ binding
    $users = DB::table('users')
        ->whereRaw('name = ?', [$name])->get();

نصائح ومحاذير

  • اضبط <code>PDO::ATTR_EMULATE_PREPARES => false</code> عند الاتصال. هذا يُجبر على Prepared Statements حقيقية على مستوى الـ driver بدلاً من المحاكاة من جانب العميل، وهو أكثر أماناً.
  • ينطبق مبدأ الحد الأدنى من الصلاحيات على مستخدمي قاعدة البيانات أيضاً — يجب أن يمتلك مستخدم DB للتطبيق فقط <code>SELECT, INSERT, UPDATE, DELETE</code> على جداوله الخاصة، وليس أبداً <code>DROP</code> أو <code>CREATE</code>.
  • التحقق من المدخلات يُكمّل الـ parameterization، وليس بديلاً عنه. تحقق من الأنواع والتنسيقات لاكتشاف الأخطاء مبكراً، لكن طبّق الـ parameterization دائماً بغض النظر.
  • ستجد الماسحات الآلية مثل SQLMap ثغرات الحقن بسرعة. شغّلها على بيئة الاختبار للتحقق من صحة إصلاحاتك.

خاتمة

منع حقن SQL يتلخص في قاعدة واحدة: لا تضع مدخل المستخدم مباشرةً في سلسلة SQL. استخدم PDO Prepared Statements مع Placeholders بالموضع أو المسمى، وطبّق القائمة البيضاء على أي معرّفات يجب أن تكون ديناميكية، وتعامل مع جميع أدوات الاستعلام الخام في الـ ORM بالحذر ذاته الذي تتعامل به مع SQL المكتوبة يدوياً. اتّبع هذه القواعد باتساق وسيصبح حقن SQL مشكلة منتهية.

#PHP #Security #Database
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.