طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 243 · أمن الويب
الدرس 12 · الوحدة 6: تقنيات الدفاع والاستجابة للهجمات

تزوير الطلبات CSRF والدفاع عنه

كيف ينفّذ موقع غريب تحويلًا من حسابك دون أن يسرق كلمتك؟ ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح كيف يستغل CSRF كوكي الجلسة التلقائي.
  • تميّز بين CSRF و XSS.
  • تحصّن الكوكي بـ SameSite و httpOnly و secure.
  • تطبّق الرمز المضاد للعمليات الحسّاسة.

١CSRF: ركوب جلسة الضحية

CSRF (تزوير الطلب عبر المواقع): المتصفح يرسل كوكي جلستك تلقائيًّا مع كل طلب لموقع، حتى لو انطلق الطلب من صفحة موقع آخر. فيصنع المهاجم صفحة ترسل طلبًا خفيًّا إلى بنكك، ومتصفحك يرفق الكوكي، فينفّذ البنك العملية ظنًّا أنها منك:

خادم البنكموقع المهاجمالضحية (مسجّل في البنك)✗ البنك يرى كوكيًّا صحيحًا فينفّذ!يفتح صفحة المهاجمنموذج خفي: تحويل مال (متصفح الضحية يرسل الكوكي تلقائيًّا)تم التحويل دون علم الضحية
المهاجم لا يسرق الكوكي؛ يستغل أن المتصفح يرسله تلقائيًّا.
XSSCSRF
ماذا يفعلينفّذ كود المهاجم في صفحتكيرسل طلبًا باسمك دون رؤية الرد
يحتاجحقن سكربت في صفحتكأن تكون مسجّل الدخول وتفتح صفحته
يرى الرد؟نعملا؛ يكفيه أن يُنفَّذ الفعل

٢الدفاع الأول: كوكي SameSite

أبسط دفاع: أخبر المتصفح بألا يرسل كوكي الجلسة مع طلبات قادمة من مواقع أخرى، بخاصية SameSite:

قيمة SameSiteالسلوك
Strictلا يُرسل الكوكي قط مع طلب من موقع آخر (الأأمن)
Lax (افتراضي حديثًا)يُرسل مع التنقّل العادي فقط، لا مع نماذج المواقع الأخرى
Noneيُرسل دائمًا (يتطلب Secure)؛ يفتح باب CSRF
res.cookie('sid', id, {  httpOnly: true,     // لا يصل إليه جافاسكربت (يقلل أثر XSS)  secure: true,       // عبر HTTPS فقط  sameSite: 'strict', // لا يُرسل مع طلبات المواقع الأخرى (يمنع CSRF)});

هذه الخصائص الثلاث معًا تحصّن كوكي الجلسة: httpOnly ضد XSS، وsecure ضد التنصّت، وsameSite ضد CSRF.

٣الدفاع الثاني: الرمز المضاد

الدفاع الكلاسيكي: رمز سرّي (CSRF token) يولّده الخادم ويضعه في نموذجك، ويُرسل مع الطلب ويُتحقق منه. المهاجم لا يعرف هذا الرمز لأنه لا يستطيع قراءة صفحتك، فيفشل طلبه:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const crypto = require('crypto');const csrfTokens = {};                 // لكل جلسة رمز مضاد app.get('/form-token', (req, res) => {  const t = crypto.randomBytes(16).toString('hex');  csrfTokens['sara'] = t;              // يُربط بالجلسة  res.json({ csrfToken: t });}); app.post('/transfer', (req, res) => {  if (req.body.csrfToken !== csrfTokens['sara'])    return res.status(403).json({ error: 'رمز CSRF غير صالح' });   // ✓ الرمز يثبت أن الطلب من صفحتنا  res.json({ ok: true, to: req.body.to });}); app.listen(3000);

أولًا نطلب رمزًا، فيُحفظ للجلسة. ثم طلب تحويل برمز خاطئ (كما يفعل المهاجم) يُرفض 403؛ لأنه لا يملك الرمز الصحيح. الطلب الشرعي من صفحتنا يحمل الرمز فينجح.

توقّع وتحقّق

CSRF يستغل:

خاصية الكوكي التي تمنع CSRF:

الرمز المضاد يعمل لأن المهاجم:

واجهة برمجية برمز في ترويسة Authorization:

الخلاصة

تحدٍّ برمجي: احمِ التحويل من CSRF

متقدم

مسار التحويل ينفّذ لأي طلب، فهو عرضة لـ CSRF. احمِه برمز مضاد: أصدر رمزًا، واطلبه في التحويل، وارفض ما لا يحمله.

المطلوب:
  • مسار GET /form-token يصدر رمزًا جديدًا ويحفظه.
  • التحويل يتطلب csrfToken مطابقًا، وإلا 403.
  • التحويل بالرمز الصحيح ينفّذ ويخصم من الرصيد.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /form-token
  2. POST /transfer {"amount": 100, "csrfToken": "forged"}
  3. GET /balance
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • form-token يصدر رمزًا نصيًّا
  • تحويل برمز مزوّر يُرفض ← 403
  • الرصيد لم ينقص (ما زال 1000)
  • يتحقق من csrfToken

💡 تلميح 1
في form-token: token = crypto.randomBytes(16).toString('hex'); واحفظه.
💡 تلميح 2
في التحويل: if (req.body.csrfToken !== token) return res.status(403)….
💡 تلميح 3
الرمز يُنشأ في الخادم ولا يعرفه المهاجم.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const crypto = require('crypto');let balance = 1000;let token = null; app.get('/form-token', (req, res) => {  token = crypto.randomBytes(16).toString('hex');  res.json({ csrfToken: token });}); app.post('/transfer', (req, res) => {  if (!token || req.body.csrfToken !== token)    return res.status(403).json({ error: 'رمز CSRF غير صالح' });  balance -= Number(req.body.amount || 0);  res.json({ ok: true, balance });});app.get('/balance', (req, res) => res.json({ balance }));app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس