المحطّة ٦: التأمين — حقن SQL و XSS والصلاحيات
منتجٌ آمنٌ: يصمد أمام الحقن و XSS ومحاولات الوصول لبيانات الغير. ⏱ نحو 26 دقيقة.
- تمنع حقن
SQLبالمعاملات المُعدّة في كل استعلام. - تمنع
XSSبالهروب من كل مخرَجٍ يحوي مدخلات المستخدم. - تغلق ثغرة الصلاحيات بربط كل عمليّةٍ بمالكها.
- تفكّر كمهاجمٍ يرسل الطلبات مباشرةً دون واجهة.
١ثلاث ثغراتٍ يجب أن يصمد أمامها مشروعك
تعلّمت في مقرّر 243 أشهر ثغرات الويب. الآن نطبّقها على منتجنا — فالتأمين ليس محطّةً أخيرة تُضاف، بل عادةٌ في كل سطر. نراجع ثلاثًا أساسيّة:
- حقن
SQL: المهاجم يحقنSQLعبر مدخلٍ غير مُعالَج. XSS: المهاجم يحقنHTML/JavaScriptيُعرَض لمستخدمٍ آخر.- ثغرة الصلاحيات: مستخدمٌ يصل إلى بيانات مستخدمٍ آخر أو يعدّلها.
٢حقن SQL: المُعدّة تغلق الباب
قارن بحثًا هشًّا يدمج النصّ، بآخر مُعدّ. جرّب الهجوم ' OR '1'='1 على الاثنين:
<?phprequire __DIR__ . '/db.php';$q = $_GET['q'] ?? '';// ❌ هشّ: دمج قيمة المستخدم في نصّ SQL$rows = $pdo->query("SELECT title FROM tasks WHERE title = '$q'")->fetchAll();echo "النتائج: " . count($rows);
<?php$pdo = new PDO('sqlite:' . __DIR__ . '/tasks.sqlite');$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
الاستعلام الهشّ أعاد كل الصفوف — الهجوم نجح.
<?phprequire __DIR__ . '/db.php';$q = $_GET['q'] ?? '';// ✅ مُعدّ: القيمة معاملٌ لا تُفسَّر كـ SQL أبدًا$st = $pdo->prepare('SELECT title FROM tasks WHERE title = ?');$st->execute([$q]);echo "النتائج: " . count($st->fetchAll());
<?php$pdo = new PDO('sqlite:' . __DIR__ . '/tasks.sqlite');$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
المُعدّ عامل النصّ كلّه قيمةً واحدة — صفر نتائج، الهجوم فشل.
٣XSS: اهرب من كل ما تعرضه
لو أضاف مستخدمٌ مهمّةً عنوانها <script>…</script> وعرضناها كما هي، نُفِّذ السكربت في متصفّح كل من يراها. الدرع: htmlspecialchars يحوّل < و> إلى رموزٍ تُعرَض نصًّا لا تُنفَّذ:
<?php$title = "<b>مهمّة</b><script>alert(1)</script>";echo "بلا هروب: $title\n";echo "مع الهروب: " . htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
٤ثغرة الصلاحيات: لا تثق بالمعرّف وحده
الخطر الأخفى: يرسل المستخدم id مهمّةٍ ليست له. لو كتبنا WHERE id = ? فقط، حذف مهمّة غيره! الدرع: اربط كل عمليّةٍ بالمالك AND user_id = ? من الجلسة — فلا يطال المستخدم إلّا ما يملك:
// ❌ يحذف أيّ مهمّةٍ بالمعرّف — ولو لم تكن له$pdo->prepare('DELETE FROM tasks WHERE id = ?')->execute([$id]); // ✅ يحذف فقط إن كانت المهمّة له$pdo->prepare('DELETE FROM tasks WHERE id = ? AND user_id = ?')->execute([$id, $_SESSION['uid']]);
توقّع وتحقّق
ما الذي يمنع حقن SQL؟
ما الذي يمنع XSS عند عرض عنوانٍ أدخله المستخدم؟
كيف نمنع مستخدمًا من حذف مهمّة غيره؟
لماذا لا يكفي إخفاء زرّ الحذف في الواجهة؟
الخلاصة
- حقن
SQL: استعلاماتٌ مُعدّةٌ دائمًا؛ لا تدمج قيَم المستخدم في نصّSQL. XSS: اهرب من كل مخرَجٍ بـhtmlspecialchars(…,ENT_QUOTES, "UTF-8").- الصلاحيات: أضِف
AND user_id= ? من الجلسة لكل تعديلٍ وحذف. - لا تثق بالواجهة؛ المهاجم يرسل الطلب مباشرةً — التحقّق في الخادم.
- لا تعرض رسائل خطأ القاعدة للمستخدم؛ سجّلها داخليًّا.
تحدٍّ برمجي: تحصين الصفحات
برمجيحصّن صفحتين من مشروعك: اجعل العرض يهرب من HTML (ضدّ XSS)، واجعل الحذف مقيّدًا بالمالك ومُعدًّا (ضدّ الحقن وثغرة الصلاحيات). سنختبر هجومًا حقيقيًّا بين مستخدمَين.
list.php: يعرض عناوين مهامّ المستخدم بعد تمريرها علىhtmlspecialchars.delete.php(POST id): يحذف المهمّة فقط إن كانت للمستخدم الحاليّ، باستعلامٍ مُعدّ، ويطبعتمّ.
👀 اعرض الاستجابة المطلوبة
POST /login.php(username=sarah&password=sarah123)- 👤 ali
POST /login.php(username=ali&password=ali123) POST /add.php(title=<b>اجتماع</b>)GET /list.php- 👤 ali
POST /delete.php(id=1) GET /list.phpPOST /delete.php(id=1)GET /list.php
- الكود يعمل بلا أخطاء أو تحذيرات
- العرض يهرب من
HTML (XSS) - لا يخرج وسمًا خامًّا قابلًا للتنفيذ
- مستخدمٌ آخر لا يستطيع حذف مهمّتك
- المالك يحذف مهمّته هو
- يستخدم htmlspecialchars في العرض
- الحذف مقيّدٌ بالمالك user_id
💡 تلميح 1
echo htmlspecialchars($t["title"], ENT_QUOTES, "UTF-8");.💡 تلميح 2
DELETE FROM tasks WHERE id = ? AND user_id = ? ثم مرّر id والجلسة.💡 تلميح 3
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?phpsession_start();require __DIR__ . '/db.php';if (!isset($_SESSION['uid'])) { echo 'سجّل الدخول'; exit; }$st = $pdo->prepare('SELECT title FROM tasks WHERE user_id = ? ORDER BY id');$st->execute([$_SESSION['uid']]);foreach ($st->fetchAll() as $t) { echo htmlspecialchars($t['title'], ENT_QUOTES, 'UTF-8') . "\n";}
<?phpsession_start();require __DIR__ . '/db.php';if (!isset($_SESSION['uid'])) { echo 'سجّل الدخول'; exit; }$st = $pdo->prepare('DELETE FROM tasks WHERE id = ? AND user_id = ?');$st->execute([$_POST['id'] ?? 0, $_SESSION['uid']]);echo 'تمّ';
<?phpsession_start();require __DIR__ . '/db.php';$st = $pdo->prepare('SELECT * FROM users WHERE username = ?');$st->execute([$_POST['username'] ?? '']);$user = $st->fetch();if ($user && password_verify($_POST['password'] ?? '', $user['pass_hash'])) { $_SESSION['uid'] = $user['id']; echo "ok";} else { echo "no"; }
<?phpsession_start();require __DIR__ . '/db.php';if (!isset($_SESSION['uid'])) { echo 'سجّل الدخول'; exit; }$title = trim($_POST['title'] ?? '');if ($title === '') { echo 'العنوان مطلوب'; exit; }$st = $pdo->prepare('INSERT INTO tasks (user_id, title, done, created_at) VALUES (?, ?, 0, ?)');$st->execute([$_SESSION['uid'], $title, date('Y-m-d')]);echo 'أُضيفت';
<?php$pdo = new PDO('sqlite:' . __DIR__ . '/tasks.sqlite');$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);