الملفات
ghaymah-exam-ahmedmohamed-S…/q2-attack-simulation /rate-limiter.js

225 أسطر
7.9 KiB
JavaScript

/**
* Rate Limiter Middleware للحماية من Brute Force
* للتطبيقات المنشورة على غيمة (ghaymah.systems)
*/
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
// ═══════════════════════════════════════════════════════════════
// إعداد Redis (من غيمة)
// ═══════════════════════════════════════════════════════════════
const redis = new Redis(process.env.GHAYMAH_REDIS_URL || 'redis://localhost:6379');
// ═══════════════════════════════════════════════════════════════
// Rate Limiter لـ Login API
// ═══════════════════════════════════════════════════════════════
const loginLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:login:'
}),
// 5 محاولات كل 15 دقيقة
windowMs: 15 * 60 * 1000,
max: 5,
// رسالة الخطأ
message: {
error: 'محاولات كثيرة جداً',
message: 'تم حظرك مؤقتاً. حاول مرة أخرى بعد 15 دقيقة',
retryAfter: 15 * 60
},
// Headers قياسية
standardHeaders: true,
legacyHeaders: false,
// تخطي الطلبات الناجحة
skipSuccessfulRequests: false,
// معالج مخصص عند تجاوز الحد
handler: (req, res, next, options) => {
// تسجيل للمراقبة
console.warn(`[SECURITY] Rate limit exceeded`, {
ip: req.ip,
path: req.path,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
// إرسال metric لـ Prometheus
if (global.metrics) {
global.metrics.rateLimitExceeded.inc({
path: '/api/login',
ip: req.ip
});
}
res.status(429).json(options.message);
},
// تحديد المفتاح (IP + username إن وجد)
keyGenerator: (req) => {
const username = req.body?.username || 'unknown';
return `${req.ip}:${username}`;
}
});
// ═══════════════════════════════════════════════════════════════
// Rate Limiter عام للـ API
// ═══════════════════════════════════════════════════════════════
const apiLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:api:'
}),
// 100 طلب كل دقيقة
windowMs: 60 * 1000,
max: 100,
message: {
error: 'طلبات كثيرة',
message: 'تجاوزت الحد المسموح. حاول لاحقاً'
},
standardHeaders: true
});
// ═══════════════════════════════════════════════════════════════
// Rate Limiter للـ endpoints الحساسة
// ═══════════════════════════════════════════════════════════════
const sensitiveEndpointLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
prefix: 'rl:sensitive:'
}),
// 10 طلبات كل 5 دقائق
windowMs: 5 * 60 * 1000,
max: 10,
message: {
error: 'وصول محدود',
message: 'هذا الـ endpoint محمي. حاول لاحقاً'
}
});
// ═══════════════════════════════════════════════════════════════
// Account Lockout بعد محاولات فاشلة
// ═══════════════════════════════════════════════════════════════
const failedAttempts = new Map(); // في الإنتاج استخدم Redis
const accountLockout = async (req, res, next) => {
const username = req.body?.username;
if (!username) return next();
const key = `lockout:${username}`;
const lockoutData = await redis.get(key);
if (lockoutData) {
const data = JSON.parse(lockoutData);
// التحقق من القفل
if (data.locked && Date.now() < data.lockedUntil) {
const remainingMinutes = Math.ceil((data.lockedUntil - Date.now()) / 60000);
return res.status(423).json({
error: 'الحساب مقفل',
message: `تم قفل الحساب بسبب محاولات فاشلة متكررة. حاول بعد ${remainingMinutes} دقيقة`,
lockedUntil: new Date(data.lockedUntil).toISOString()
});
}
}
next();
};
// تسجيل المحاولة الفاشلة
const recordFailedAttempt = async (username) => {
const key = `lockout:${username}`;
let data = await redis.get(key);
if (data) {
data = JSON.parse(data);
data.attempts += 1;
} else {
data = { attempts: 1, locked: false };
}
// قفل بعد 5 محاولات
if (data.attempts >= 5) {
data.locked = true;
data.lockedUntil = Date.now() + (30 * 60 * 1000); // 30 دقيقة
console.warn(`[SECURITY] Account locked: ${username}`, {
attempts: data.attempts,
lockedUntil: new Date(data.lockedUntil).toISOString()
});
}
// حفظ لمدة ساعة
await redis.setex(key, 3600, JSON.stringify(data));
return data;
};
// مسح المحاولات بعد نجاح الدخول
const clearFailedAttempts = async (username) => {
await redis.del(`lockout:${username}`);
};
// ═══════════════════════════════════════════════════════════════
// Exports
// ═══════════════════════════════════════════════════════════════
module.exports = {
loginLimiter,
apiLimiter,
sensitiveEndpointLimiter,
accountLockout,
recordFailedAttempt,
clearFailedAttempts
};
// ═══════════════════════════════════════════════════════════════
// مثال الاستخدام في Express
// ═══════════════════════════════════════════════════════════════
/*
const express = require('express');
const {
loginLimiter,
apiLimiter,
accountLockout,
recordFailedAttempt,
clearFailedAttempts
} = require('./rate-limiter');
const app = express();
// تطبيق على جميع الـ APIs
app.use('/api', apiLimiter);
// تطبيق على Login
app.post('/api/login',
loginLimiter,
accountLockout,
async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);
if (!user) {
await recordFailedAttempt(username);
return res.status(401).json({ error: 'بيانات خاطئة' });
}
// نجاح - مسح المحاولات الفاشلة
await clearFailedAttempts(username);
const token = generateToken(user);
res.json({ token });
}
);
*/