From 35210291d05790dfa436db330053a60b2f9c9303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 13:36:10 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D1=88=D0=B0=D0=B3=202=20=E2=80=94?= =?UTF-8?q?=20=D0=BB=D0=B8=D0=BC=D0=B8=D1=82=D0=B5=D1=80=D1=8B=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B2=20src/middl?= =?UTF-8?q?eware/rateLimit.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 23 ++----------------- src/middleware/rateLimit.js | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 src/middleware/rateLimit.js diff --git a/server.js b/server.js index b222f8e..0083437 100644 --- a/server.js +++ b/server.js @@ -2,7 +2,8 @@ const express = require('express'); const cookieParser = require('cookie-parser'); const path = require('path'); const helmet = require('helmet'); -const { rateLimit } = require('express-rate-limit'); +// rateLimit.js: mutationLimiter (POST-роуты) и exportLimiter (GET /export) +const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit'); const { doubleCsrf } = require('csrf-csrf'); require('dotenv').config(); const { checkConnection } = require('./src/db'); @@ -23,26 +24,6 @@ app.set('views', path.join(__dirname, 'views')); // Referrer-Policy и пр. CSP отключён явно — EJS использует inline-стили/скрипты. app.use(helmet({ contentSecurityPolicy: false })); -// ── Rate limiting ────────────────────────────────────────────────────────────── -// Мутирующие POST-маршруты: не более 30 запросов за 1 минуту с одного IP. -// Это защищает от случайного flood и простых скриптовых атак. -const mutationLimiter = rateLimit({ - windowMs: 60_000, // окно 1 минута - limit: 30, // макс 30 POST-запросов - standardHeaders: 'draft-7', // Retry-After в заголовках - legacyHeaders: false, - message: { error: 'Слишком много запросов. Подождите минуту.' }, -}); - -// /export доступен публично — ограничиваем отдельно, мягче (по IP) -const exportLimiter = rateLimit({ - windowMs: 60_000, - limit: 20, - standardHeaders: 'draft-7', - legacyHeaders: false, - message: 'Слишком много запросов к /export.', -}); - app.use(express.static(path.join(__dirname, 'public'))); app.use(express.urlencoded({ extended: true, limit: '32kb' })); app.use(cookieParser()); diff --git a/src/middleware/rateLimit.js b/src/middleware/rateLimit.js new file mode 100644 index 0000000..a703b5a --- /dev/null +++ b/src/middleware/rateLimit.js @@ -0,0 +1,45 @@ +/** + * src/middleware/rateLimit.js — настройки ограничения частоты запросов. + * + * Зачем вынесено отдельно: + * - Лимиты можно менять в одном месте, не трогая роуты. + * - Легко подключать только нужный лимитер к конкретному роуту. + * - Упрощает тестирование — можно заменить на no-op лимитер в тестах. + * + * express-rate-limit считает запросы по IP (по умолчанию — req.ip). + * Если приложение стоит за reverse-proxy (nginx/traefik), нужно + * настроить app.set('trust proxy', 1) чтобы X-Forwarded-For работал корректно. + */ + +'use strict'; + +const { rateLimit } = require('express-rate-limit'); + +// ── mutationLimiter — для мутирующих POST-запросов ──────────────────────────── +// Применяется к: POST /add, /edit/:id, /delete/:id, /admin/limit/:id +// +// Лимит 30 запросов за минуту с одного IP — достаточно для ручного +// пользования и при этом блокирует простые скриптовые атаки. +// +// При превышении лимита — 429 Too Many Requests. +const mutationLimiter = rateLimit({ + windowMs: 60_000, // скользящее окно: 1 минута + limit: 30, // не более 30 POST-запросов за окно + standardHeaders: 'draft-7', // добавляет заголовки RateLimit-* и Retry-After + legacyHeaders: false, // не добавлять устаревшие X-RateLimit-* заголовки + message: { error: 'Слишком много запросов. Подождите минуту.' }, +}); + +// ── exportLimiter — для публичного GET /export ──────────────────────────────── +// /export не требует авторизации (ТЗ) — поэтому ограничиваем жёстче. +// 20 запросов в минуту достаточно для автоматической синхронизации +// (nginx/firewall обычно опрашивают раз в несколько минут). +const exportLimiter = rateLimit({ + windowMs: 60_000, + limit: 20, + standardHeaders: 'draft-7', + legacyHeaders: false, + message: 'Слишком много запросов к /export. Подождите минуту.', +}); + +module.exports = { mutationLimiter, exportLimiter };