From 2b09cf05ebf48716bf0b29cf470f81104ba82189 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Fri, 6 Mar 2026 23:55:29 +0000 Subject: [PATCH] fix(#19): Implement rate limiting for auth and write-heavy endpoints --- backend/src/__tests__/rateLimit.test.js | 65 +++++++++++++++++++++++++ backend/src/middleware/rateLimit.js | 52 ++++++++++++++++++++ backend/src/server.js | 18 ++++--- 3 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 backend/src/__tests__/rateLimit.test.js create mode 100644 backend/src/middleware/rateLimit.js diff --git a/backend/src/__tests__/rateLimit.test.js b/backend/src/__tests__/rateLimit.test.js new file mode 100644 index 0000000..367bf64 --- /dev/null +++ b/backend/src/__tests__/rateLimit.test.js @@ -0,0 +1,65 @@ +import { rateLimit, authRateLimit } from '../middleware/rateLimit.js'; +import express from 'express'; +import request from 'supertest'; + +describe('Rate Limit Middleware', () => { + let app; + + beforeEach(() => { + app = express(); + app.use(express.json()); + }); + + it('should allow requests within limit', (done) => { + const middleware = rateLimit({ max: 2, windowMs: 1000 }); + + app.get('/test', middleware, (req, res) => { + res.status(200).json({ message: 'OK' }); + }); + + request(app) + .get('/test') + .expect(200) + .end(done); + }); + + it('should block requests exceeding limit', (done) => { + const middleware = rateLimit({ max: 1, windowMs: 1000 }); + + app.get('/test', middleware, (req, res) => { + res.status(200).json({ message: 'OK' }); + }); + + // Erster Request sollte erfolgreich sein + request(app) + .get('/test') + .expect(200) + .end(() => { + // Zweiter Request sollte blockiert werden + request(app) + .get('/test') + .expect(429) + .end(done); + }); + }); + + it('should apply auth rate limiting correctly', (done) => { + const middleware = authRateLimit({ max: 1, windowMs: 1000 }); + + app.get('/auth-test', middleware, (req, res) => { + res.status(200).json({ message: 'OK' }); + }); + + // Erster Request sollte erfolgreich sein + request(app) + .get('/auth-test') + .expect(200) + .end(() => { + // Zweiter Request sollte blockiert werden + request(app) + .get('/auth-test') + .expect(429) + .end(done); + }); + }); +}); \ No newline at end of file diff --git a/backend/src/middleware/rateLimit.js b/backend/src/middleware/rateLimit.js new file mode 100644 index 0000000..980b4f4 --- /dev/null +++ b/backend/src/middleware/rateLimit.js @@ -0,0 +1,52 @@ +// Einfache Rate-Limiting-Middleware ohne externe Abhängigkeiten + +// Konfigurierbare Limits über Umgebungsvariablen +const DEFAULT_WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000; // 15 Minuten +const DEFAULT_MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100; +const DEFAULT_AUTH_WINDOW_MS = parseInt(process.env.RATE_LIMIT_AUTH_WINDOW_MS) || 5 * 60 * 1000; // 5 Minuten +const DEFAULT_AUTH_MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_AUTH_MAX_REQUESTS) || 5; + +// Speicher für IP-Adressen und Request-Zähler +const rateLimits = new Map(); + +const rateLimit = (options = {}) => { + const windowMs = options.windowMs || DEFAULT_WINDOW_MS; + const maxRequests = options.max || DEFAULT_MAX_REQUESTS; + + return (req, res, next) => { + const key = req.ip; // IP-Adresse als Schlüssel + const now = Date.now(); + + if (!rateLimits.has(key)) { + rateLimits.set(key, { count: 1, windowStart: now }); + } else { + const data = rateLimits.get(key); + if (now - data.windowStart > windowMs) { + // Fenster abgelaufen, neues Fenster starten + rateLimits.set(key, { count: 1, windowStart: now }); + } else { + // Prüfen, ob Limit erreicht wurde + if (data.count >= maxRequests) { + return res.status(429).json({ + error: 'Too many requests', + retryAfter: Math.ceil((windowMs - (now - data.windowStart)) / 1000) + }); + } + // Zähler erhöhen + rateLimits.set(key, { count: data.count + 1, windowStart: data.windowStart }); + } + } + + next(); + }; +}; + +// Spezielle Middleware für Auth-Endpunkte mit kürzerem Fenster und niedrigerem Limit +const authRateLimit = (options = {}) => { + const windowMs = options.windowMs || DEFAULT_AUTH_WINDOW_MS; + const maxRequests = options.max || DEFAULT_AUTH_MAX_REQUESTS; + + return rateLimit({ windowMs, max: maxRequests }); +}; + +module.exports = { rateLimit, authRateLimit }; \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js index d0087c6..5518094 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -8,6 +8,7 @@ import addressRoutes from './routes/addresses.js'; import contactRoutes from './routes/contacts.js'; import profileRoutes from './routes/profile.js'; import logger from './middleware/logger.js'; +const { rateLimit, authRateLimit } = require('./middleware/rateLimit.js'); dotenv.config(); @@ -33,13 +34,16 @@ app.get('/metrics', (_req, res) => { }); }); -app.use('/auth', authRoutes); -app.use('/requests', helpRequestRoutes); -app.use('/offers', offerRoutes); -app.use('/reviews', reviewRoutes); -app.use('/addresses', addressRoutes); -app.use('/contacts', contactRoutes); -app.use('/profile', profileRoutes); +// Rate limiting für Auth-Endpunkte +app.use('/auth', authRateLimit(), authRoutes); + +// Rate limiting für write-heavy Endpunkte +app.use('/requests', rateLimit({ max: 50 }), helpRequestRoutes); +app.use('/offers', rateLimit({ max: 50 }), offerRoutes); +app.use('/reviews', rateLimit({ max: 50 }), reviewRoutes); +app.use('/addresses', rateLimit({ max: 50 }), addressRoutes); +app.use('/contacts', rateLimit({ max: 50 }), contactRoutes); +app.use('/profile', rateLimit({ max: 50 }), profileRoutes); const port = Number(process.env.PORT || 3000); app.listen(port, () => {