تزوير الطلبات CSRF والدفاع عنه
كيف ينفّذ موقع غريب تحويلًا من حسابك دون أن يسرق كلمتك؟ ⏱ نحو 45 دقيقة.
- تشرح كيف يستغل
CSRFكوكي الجلسة التلقائي. - تميّز بين
CSRFوXSS. - تحصّن الكوكي بـ
SameSiteوhttpOnlyوsecure. - تطبّق الرمز المضاد للعمليات الحسّاسة.
١CSRF: ركوب جلسة الضحية
CSRF (تزوير الطلب عبر المواقع): المتصفح يرسل كوكي جلستك تلقائيًّا مع كل طلب لموقع، حتى لو انطلق الطلب من صفحة موقع آخر. فيصنع المهاجم صفحة ترسل طلبًا خفيًّا إلى بنكك، ومتصفحك يرفق الكوكي، فينفّذ البنك العملية ظنًّا أنها منك:
| XSS | CSRF | |
|---|---|---|
| ماذا يفعل | ينفّذ كود المهاجم في صفحتك | يرسل طلبًا باسمك دون رؤية الرد |
| يحتاج | حقن سكربت في صفحتك | أن تكون مسجّل الدخول وتفتح صفحته |
| يرى الرد؟ | نعم | لا؛ يكفيه أن يُنفَّذ الفعل |
٢الدفاع الأول: كوكي 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: المتصفح يرسل كوكي جلستك تلقائيًّا، فينفّذ المهاجم فعلًا باسمك.SameSite=Strict/Laxيمنع إرسال الكوكي مع طلبات المواقع الأخرى.- الرمز المضاد سرّ في نموذجك لا يعرفه المهاجم، يُتحقق منه مع الطلب.
- الواجهات برمز في ترويسة
Authorizationمحصّنة ضدCSRFأصلًا.
تحدٍّ برمجي: احمِ التحويل من CSRF
متقدممسار التحويل ينفّذ لأي طلب، فهو عرضة لـ CSRF. احمِه برمز مضاد: أصدر رمزًا، واطلبه في التحويل، وارفض ما لا يحمله.
- مسار
GET /form-tokenيصدر رمزًا جديدًا ويحفظه. - التحويل يتطلب
csrfTokenمطابقًا، وإلا 403. - التحويل بالرمز الصحيح ينفّذ ويخصم من الرصيد.
👀 اعرض الاستجابات المطلوبة
GET /form-tokenPOST /transfer{"amount": 100, "csrfToken": "forged"}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);