diff --git a/server.js b/server.js index b3e93b0..98cce3d 100644 --- a/server.js +++ b/server.js @@ -27,7 +27,7 @@ const { initAuth, requireAdmin } = require('./src/auth'); const { aggregateCIDRs } = require('./src/validators'); const q = require('./src/queries'); const { MOCK_USERS, backUrl } = require('./src/config'); -const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit'); +const { mutationLimiter, exportLimiter, authLimiter } = require('./src/middleware/rateLimit'); const { initCsrf } = require('./src/middleware/csrf'); const { createSessionMiddleware } = require('./src/middleware/session'); const { CSP_DIRECTIVES } = require('./src/middleware/csp'); @@ -97,6 +97,7 @@ async function start() { doubleCsrfProtection, generateCsrfToken, MOCK_USERS, + authLimiter, })); // ── Все маршруты ниже требуют авторизации (auth.middleware) ──────────────── diff --git a/src/middleware/rateLimit.js b/src/middleware/rateLimit.js index a703b5a..ba96ffd 100644 --- a/src/middleware/rateLimit.js +++ b/src/middleware/rateLimit.js @@ -30,8 +30,7 @@ const mutationLimiter = rateLimit({ message: { error: 'Слишком много запросов. Подождите минуту.' }, }); -// ── exportLimiter — для публичного GET /export ──────────────────────────────── -// /export не требует авторизации (ТЗ) — поэтому ограничиваем жёстче. +// ── exportLimiter — для GET /export ────────────────────────────────────────── // 20 запросов в минуту достаточно для автоматической синхронизации // (nginx/firewall обычно опрашивают раз в несколько минут). const exportLimiter = rateLimit({ @@ -42,4 +41,16 @@ const exportLimiter = rateLimit({ message: 'Слишком много запросов к /export. Подождите минуту.', }); -module.exports = { mutationLimiter, exportLimiter }; +// ── authLimiter — для POST /login, POST /dev-login, GET /callback ───────────── +// Защита от брутфорса и нагрузки на Keycloak token endpoint. +// 10 попыток в 5 минут с одного IP — достаточно для нормального пользования, +// жёстко блокирует автоматические переборы. +const authLimiter = rateLimit({ + windowMs: 5 * 60_000, // скользящее окно: 5 минут + limit: 10, // не более 10 попыток входа за окно + standardHeaders: 'draft-7', + legacyHeaders: false, + message: { error: 'Слишком много попыток входа. Попробуйте через 5 минут.' }, +}); + +module.exports = { mutationLimiter, exportLimiter, authLimiter }; diff --git a/src/routes/auth.js b/src/routes/auth.js index a7d70bf..c500751 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -39,7 +39,7 @@ function safeReturn(target) { return '/'; } -function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }) { +function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS, authLimiter }) { const router = Router(); // ── GET /login ───────────────────────────────────────────────────────────── @@ -63,7 +63,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER }); // ── POST /login (только mock-режим) ──────────────────────────────────────── - router.post('/login', doubleCsrfProtection, (req, res) => { + router.post('/login', authLimiter, doubleCsrfProtection, (req, res) => { if (auth.isOidc) return res.redirect('/login'); const user = MOCK_USERS.find(u => u.id === req.body.user); @@ -92,7 +92,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER }); // ── GET /callback (только OIDC-режим) ────────────────────────────────────── - router.get('/callback', async (req, res) => { + router.get('/callback', authLimiter, async (req, res) => { if (!auth.isOidc) return res.redirect('/login'); const { code, state, error } = req.query; @@ -152,7 +152,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER }); // ── POST /dev-login ──────────────────────────────────────────────────────── - router.post('/dev-login', doubleCsrfProtection, (req, res) => { + router.post('/dev-login', authLimiter, doubleCsrfProtection, (req, res) => { if (!auth.devLoginEnabled) return res.status(404).send('Not Found'); if (auth.DEV_SECRET && req.session.devKey !== auth.DEV_SECRET) {