تحديد المعدل وكشف التخمين
كيف نمنع آلة تجرّب ألف كلمة مرور في الدقيقة من اقتحام الحسابات؟ ⏱ نحو 40 دقيقة.
- تشرح هجوم التخمين عبر الشبكة وأنواعه.
- تطبّق تحديد المعدل وتردّ 429.
- توازن بين القفل والتأخير وأثرهما على المستخدم الحقيقي.
- تتجنّب كشف أيّ حقل خطأ في رسائل الفشل.
١هجوم التخمين
كلمة المرور المجزّأة بـ bcrypt (الدرس 11) تقاوم التخمين دون اتصال، لكن يبقى التخمين عبر الشبكة: برنامج يجرّب آلاف كلمات المرور على صفحة الدخول. إن سمح الخادم بمحاولات بلا حدّ، فالوقت وحده يفصل المهاجم عن الحساب.
| نوع التخمين | الوصف | الدفاع |
|---|---|---|
| القوة الغاشمة | تجربة كل الاحتمالات | كلمات مرور طويلة + تحديد المعدل |
| القاموس | تجربة كلمات شائعة | منع الكلمات الضعيفة |
| حشو بيانات الاعتماد | تجربة كلمات مسرَّبة من مواقع أخرى | تحديد المعدل + مصادقة ثنائية |
الدفاع الأساسي: تحديد المعدل — حدّ لعدد المحاولات في فترة زمنية. فحتى لو كانت كلمة المرور ضعيفة، لا يملك المهاجم إلا محاولات قليلة قبل أن يُوقَف.
٢تحديد المعدل عمليًّا
نحسب محاولات كل مصدر في نافذة زمنية، فإن تجاوزت الحدّ نردّ 429 (طلبات كثيرة). جرّب: المحاولتان الأوليان تُفحصان، والثالثة فصاعدًا تُمنع:
const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const hits = {}; // عدّاد المحاولات لكل مفتاح (IP مثلًا)const LIMIT = 3, WINDOW = 60000; // 3 محاولات في الدقيقة function rateLimit(req, res, next) { const key = req.ip || 'test'; const now = Date.now(); hits[key] = (hits[key] || []).filter(t => now - t < WINDOW); if (hits[key].length >= LIMIT) return res.status(429).json({ error: 'محاولات كثيرة، حاول لاحقًا' }); hits[key].push(now); next();} app.post('/login', rateLimit, (req, res) => { const ok = req.body.pass === 's@rah123'; res.status(ok ? 200 : 401).json({ granted: ok });}); app.listen(3000);
أول محاولتين رُدّتا 401 (خطأ لكن مسموح)، والثالثة 429 (مُنعت)، والرابعة — ولو بكلمة صحيحة — مُنعت أيضًا لأن النافذة امتلأت. المهاجم خُنق قبل أن يصل.
| التقنية | الفكرة | ملحوظة |
|---|---|---|
| تحديد المعدل | حدّ محاولات لكل مفتاح/نافذة | المفتاح: IP أو حساب أو كليهما |
| القفل المؤقت | إيقاف الحساب بعد N فشل | احذر قفل مستخدمين حقيقيين |
| التأخير التصاعدي | كل فشل يزيد زمن الانتظار | يبطئ الآلة دون قفل |
| المصادقة الثنائية | عامل ثانٍ غير كلمة المرور | أقوى دفاع ضد الحشو |
٣اعتبارات التطبيق
تحديد المعدل أداة حادة؛ طبّقها بحكمة:
وفي الإنتاج تُستخدم مكتبات جاهزة مثل express-rate-limit بدل الكتابة اليدوية، لتضبط النوافذ والتخزين الموزّع.
توقّع وتحقّق
رمز الحالة لتجاوز حدّ المحاولات:
تحديد المعدل يحمي أساسًا من:
خطر قفل الحساب نهائيًّا:
أقوى دفاع ضد حشو بيانات الاعتماد:
الخلاصة
- التخمين عبر الشبكة يبقى خطرًا حتى مع تجزئة كلمة المرور.
- تحديد المعدل: حدّ محاولات لكل مفتاح في نافذة، وتجاوزه يردّ 429.
- القفل النهائي يُستغل لحجب مستخدم حقيقي؛ التأخير التصاعدي أخف.
- ادمج
IPمع الحساب، وأضفRetry-After، وسجّل التجاوزات، ولا تكشف أيّ حقل خطأ.
تحدٍّ برمجي: حدّد معدل تسجيل الدخول
متقدمهذا الخادم يقبل محاولات دخول بلا حدّ. أضف تحديد معدل يسمح بثلاث محاولات فقط، ثم يردّ 429.
- أضف وسيطًا يسمح بـ 3 محاولات ثم يردّ 429 للرابعة.
- المحاولات الثلاث الأولى تُفحص عاديًّا
(200/401). - طبّق الوسيط على مسار الدخول.
👀 اعرض الاستجابات المطلوبة
POST /login{"pass": "a"}POST /login{"pass": "b"}POST /login{"pass": "c"}POST /login{"pass": "secret"}
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- المحاولة 1 تُفحص ← 401
- المحاولة 2 تُفحص ← 401
- المحاولة 3 تُفحص ← 401
- المحاولة 4 تُمنع ← 429
- يردّ 429 عند التجاوز
💡 تلميح 1
💡 تلميح 2
return res.status(429)… قبل فحص الكلمة.💡 تلميح 3
app.post('/login', rateLimit, (req,res)=>…).حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); let attempts = 0;const hits = [];const LIMIT = 3, WINDOW = 60000;function rateLimit(req, res, next) { const now = Date.now(); while (hits.length && now - hits[0] >= WINDOW) hits.shift(); if (hits.length >= LIMIT) return res.status(429).json({ error: 'محاولات كثيرة' }); hits.push(now); next();}app.post('/login', rateLimit, (req, res) => { attempts++; const ok = req.body.pass === 'secret'; res.status(ok ? 200 : 401).json({ granted: ok, attempts });});app.get('/attempts', (req, res) => res.json({ attempts }));app.listen(3000);