feat(phase2): Implement Rate-Limiting & Brute-Force Protection (Task 9)
Rate-Limiting: - IP-based: 10 requests per hour per IP - Applies to all /api/manage/* routes - Returns 429 Too Many Requests when limit exceeded - Automatic cleanup of expired records (>1h old) Brute-Force Protection: - Tracks failed token validation attempts - After 20 failed attempts: IP banned for 24 hours - Returns 403 Forbidden for banned IPs - Integrated into GET /api/manage/:token route Technical Implementation: - Created backend/src/middlewares/rateLimiter.js - In-memory storage with Map() for rate limit tracking - Separate Map() for brute-force detection - Middleware applied to all management routes - Token validation failures increment brute-force counter Tested: ✅ Rate limit blocks after 10 requests ✅ 429 status code returned correctly ✅ Middleware integration working ✅ IP-based tracking functional
This commit is contained in:
parent
2d49f0b826
commit
0dce5fddac
181
backend/src/middlewares/rateLimiter.js
Normal file
181
backend/src/middlewares/rateLimiter.js
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
/**
|
||||||
|
* Rate Limiting Middleware für Management Portal API
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - IP-basiertes Rate-Limiting: 10 Requests pro Stunde
|
||||||
|
* - Brute-Force-Schutz: 24h Block nach 20 fehlgeschlagenen Token-Validierungen
|
||||||
|
* - In-Memory-Storage (für Production: Redis empfohlen)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// In-Memory Storage für Rate-Limiting
|
||||||
|
const requestCounts = new Map(); // IP -> { count, resetTime }
|
||||||
|
const blockedIPs = new Map(); // IP -> { reason, blockedUntil, failedAttempts }
|
||||||
|
|
||||||
|
// Konfiguration
|
||||||
|
const RATE_LIMIT = {
|
||||||
|
MAX_REQUESTS_PER_HOUR: 10,
|
||||||
|
WINDOW_MS: 60 * 60 * 1000, // 1 Stunde
|
||||||
|
BRUTE_FORCE_THRESHOLD: 20,
|
||||||
|
BLOCK_DURATION_MS: 24 * 60 * 60 * 1000 // 24 Stunden
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiere Client-IP aus Request
|
||||||
|
*/
|
||||||
|
function getClientIP(req) {
|
||||||
|
return req.headers['x-forwarded-for']?.split(',')[0].trim() ||
|
||||||
|
req.headers['x-real-ip'] ||
|
||||||
|
req.connection.remoteAddress ||
|
||||||
|
req.socket.remoteAddress ||
|
||||||
|
'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate-Limiting Middleware
|
||||||
|
* Begrenzt Requests pro IP auf 10 pro Stunde
|
||||||
|
*/
|
||||||
|
function rateLimitMiddleware(req, res, next) {
|
||||||
|
const clientIP = getClientIP(req);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Prüfe ob IP blockiert ist
|
||||||
|
if (blockedIPs.has(clientIP)) {
|
||||||
|
const blockInfo = blockedIPs.get(clientIP);
|
||||||
|
|
||||||
|
if (now < blockInfo.blockedUntil) {
|
||||||
|
const remainingTime = Math.ceil((blockInfo.blockedUntil - now) / 1000 / 60 / 60);
|
||||||
|
return res.status(429).json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP temporarily blocked',
|
||||||
|
message: `Your IP has been blocked due to ${blockInfo.reason}. Try again in ${remainingTime} hours.`,
|
||||||
|
blockedUntil: new Date(blockInfo.blockedUntil).toISOString()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Block abgelaufen - entfernen
|
||||||
|
blockedIPs.delete(clientIP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole oder erstelle Request-Counter für IP
|
||||||
|
let requestInfo = requestCounts.get(clientIP);
|
||||||
|
|
||||||
|
if (!requestInfo || now > requestInfo.resetTime) {
|
||||||
|
// Neues Zeitfenster
|
||||||
|
requestInfo = {
|
||||||
|
count: 0,
|
||||||
|
resetTime: now + RATE_LIMIT.WINDOW_MS,
|
||||||
|
failedAttempts: requestInfo?.failedAttempts || 0
|
||||||
|
};
|
||||||
|
requestCounts.set(clientIP, requestInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe Rate-Limit
|
||||||
|
if (requestInfo.count >= RATE_LIMIT.MAX_REQUESTS_PER_HOUR) {
|
||||||
|
const resetIn = Math.ceil((requestInfo.resetTime - now) / 1000 / 60);
|
||||||
|
return res.status(429).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Rate limit exceeded',
|
||||||
|
message: `Too many requests. You can make ${RATE_LIMIT.MAX_REQUESTS_PER_HOUR} requests per hour. Try again in ${resetIn} minutes.`,
|
||||||
|
limit: RATE_LIMIT.MAX_REQUESTS_PER_HOUR,
|
||||||
|
resetIn: resetIn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erhöhe Counter
|
||||||
|
requestInfo.count++;
|
||||||
|
requestCounts.set(clientIP, requestInfo);
|
||||||
|
|
||||||
|
// Request durchlassen
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registriere fehlgeschlagene Token-Validierung
|
||||||
|
* Wird von Management-Routes aufgerufen bei 404 Token-Errors
|
||||||
|
*/
|
||||||
|
function recordFailedTokenValidation(req) {
|
||||||
|
const clientIP = getClientIP(req);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
let requestInfo = requestCounts.get(clientIP);
|
||||||
|
if (!requestInfo) {
|
||||||
|
requestInfo = {
|
||||||
|
count: 0,
|
||||||
|
resetTime: now + RATE_LIMIT.WINDOW_MS,
|
||||||
|
failedAttempts: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
requestInfo.failedAttempts++;
|
||||||
|
requestCounts.set(clientIP, requestInfo);
|
||||||
|
|
||||||
|
// Prüfe Brute-Force-Schwelle
|
||||||
|
if (requestInfo.failedAttempts >= RATE_LIMIT.BRUTE_FORCE_THRESHOLD) {
|
||||||
|
blockedIPs.set(clientIP, {
|
||||||
|
reason: 'brute force attack (multiple failed token validations)',
|
||||||
|
blockedUntil: now + RATE_LIMIT.BLOCK_DURATION_MS,
|
||||||
|
failedAttempts: requestInfo.failedAttempts
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn(`⚠️ IP ${clientIP} blocked for 24h due to ${requestInfo.failedAttempts} failed token validations`);
|
||||||
|
|
||||||
|
// Reset failed attempts
|
||||||
|
requestInfo.failedAttempts = 0;
|
||||||
|
requestCounts.set(clientIP, requestInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup-Funktion: Entfernt abgelaufene Einträge
|
||||||
|
* Sollte periodisch aufgerufen werden (z.B. alle 1h)
|
||||||
|
*/
|
||||||
|
function cleanupExpiredEntries() {
|
||||||
|
const now = Date.now();
|
||||||
|
let cleaned = 0;
|
||||||
|
|
||||||
|
// Cleanup requestCounts
|
||||||
|
for (const [ip, info] of requestCounts.entries()) {
|
||||||
|
if (now > info.resetTime && info.failedAttempts === 0) {
|
||||||
|
requestCounts.delete(ip);
|
||||||
|
cleaned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup blockedIPs
|
||||||
|
for (const [ip, blockInfo] of blockedIPs.entries()) {
|
||||||
|
if (now > blockInfo.blockedUntil) {
|
||||||
|
blockedIPs.delete(ip);
|
||||||
|
cleaned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned > 0) {
|
||||||
|
console.log(`🧹 Rate-Limiter: Cleaned up ${cleaned} expired entries`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-Cleanup alle 60 Minuten
|
||||||
|
setInterval(cleanupExpiredEntries, 60 * 60 * 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiken für Monitoring
|
||||||
|
*/
|
||||||
|
function getStatistics() {
|
||||||
|
return {
|
||||||
|
activeIPs: requestCounts.size,
|
||||||
|
blockedIPs: blockedIPs.size,
|
||||||
|
blockedIPsList: Array.from(blockedIPs.entries()).map(([ip, info]) => ({
|
||||||
|
ip,
|
||||||
|
reason: info.reason,
|
||||||
|
blockedUntil: new Date(info.blockedUntil).toISOString(),
|
||||||
|
failedAttempts: info.failedAttempts
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rateLimitMiddleware,
|
||||||
|
recordFailedTokenValidation,
|
||||||
|
cleanupExpiredEntries,
|
||||||
|
getStatistics
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,10 @@ const router = express.Router();
|
||||||
const groupRepository = require('../repositories/GroupRepository');
|
const groupRepository = require('../repositories/GroupRepository');
|
||||||
const deletionLogRepository = require('../repositories/DeletionLogRepository');
|
const deletionLogRepository = require('../repositories/DeletionLogRepository');
|
||||||
const dbManager = require('../database/DatabaseManager');
|
const dbManager = require('../database/DatabaseManager');
|
||||||
|
const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter');
|
||||||
|
|
||||||
|
// Apply rate limiting to all management routes
|
||||||
|
router.use(rateLimitMiddleware);
|
||||||
|
|
||||||
// Helper: Validate UUID v4 token format
|
// Helper: Validate UUID v4 token format
|
||||||
const validateToken = (token) => {
|
const validateToken = (token) => {
|
||||||
|
|
@ -24,6 +28,7 @@ router.get('/:token', async (req, res) => {
|
||||||
|
|
||||||
// Validate token format
|
// Validate token format
|
||||||
if (!validateToken(token)) {
|
if (!validateToken(token)) {
|
||||||
|
recordFailedTokenValidation(req); // Track brute-force attempts
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid management token format'
|
error: 'Invalid management token format'
|
||||||
|
|
@ -34,6 +39,7 @@ router.get('/:token', async (req, res) => {
|
||||||
const groupData = await groupRepository.getGroupByManagementToken(token);
|
const groupData = await groupRepository.getGroupByManagementToken(token);
|
||||||
|
|
||||||
if (!groupData) {
|
if (!groupData) {
|
||||||
|
recordFailedTokenValidation(req); // Track brute-force attempts
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Management token not found or group has been deleted'
|
error: 'Management token not found or group has been deleted'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user