add the main file structure requirements
هذا الالتزام موجود في:
224
q2-attack-simulation /rate-limiter.js
Normal file
224
q2-attack-simulation /rate-limiter.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
);
|
||||
*/
|
||||
المرجع في مشكلة جديدة
حظر مستخدم