From 7d115e18455615663ed56eb08f13331999da430f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 16 Nov 2025 14:49:23 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=B2=D0=B5=D0=BB=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20rate=20limits=20=D0=B4=D0=BB=D1=8F=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B8=D0=B7=D0=B2=D0=BE=D0=B4=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Основной лимит: 100 → 1000 запросов за 15 минут - API лимит: 500 → 2000 запросов за 5 минут --- backend/server.js | 90 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/backend/server.js b/backend/server.js index b93e47b..ced9865 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,6 +8,7 @@ const session = require("express-session"); const SQLiteStore = require("connect-sqlite3")(session); const path = require("path"); const helmet = require("helmet"); +const cors = require("cors"); const rateLimit = require("express-rate-limit"); const bodyParser = require("body-parser"); const multer = require("multer"); @@ -25,6 +26,38 @@ const PORT = process.env.PORT || 3001; // Доверяем всем прокси (nginx proxy manager должен передавать X-Forwarded-For) app.set("trust proxy", true); +// Конфигурация CORS (защита от CSRF) +const corsOptions = { + origin: function (origin, callback) { + // Список разрешённых доменов + const allowedOrigins = [ + "http://localhost:3000", + "http://localhost:5173", // Vite dev server + "http://localhost:5174", + "http://127.0.0.1:3000", + "http://127.0.0.1:5173", + ]; + + // В продакшене добавить домены вашего сайта + if (process.env.ALLOWED_ORIGINS) { + allowedOrigins.push(...process.env.ALLOWED_ORIGINS.split(",")); + } + + // Если origin не указан (например, для мобильного приложения), разрешаем + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error("CORS не разрешен для этого домена")); + } + }, + credentials: true, // разрешить куки + optionsSuccessStatus: 200, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], + allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"], +}; + +app.use(cors(corsOptions)); + // Создаем директорию для аватарок, если её нет const uploadsDir = path.join(__dirname, "public", "uploads"); if (!fs.existsSync(uploadsDir)) { @@ -185,12 +218,42 @@ app.use( }) ); -// Ограничение запросов (отключено для разработки) -// const limiter = rateLimit({ -// windowMs: 15 * 60 * 1000, // 15 минут -// max: 100, // максимум 100 запросов с одного IP -// }); -// app.use(limiter); +// Ограничение запросов (включено для защиты от DDoS и брутфорса) +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 минут + max: 1000, // максимум 1000 запросов с одного IP + standardHeaders: true, // вернёт информацию о rate limit в `RateLimit-*` заголовки + legacyHeaders: false, // отключить `X-RateLimit-*` заголовки + message: "Слишком много запросов с этого IP адреса, пожалуйста попробуйте позже.", + skip: (req) => { + // Пропускаем health check запросы + return req.path === "/health" || req.path === "/manifest.json"; + }, +}); +app.use(limiter); + +// Специальный rate limiter для защиты от брутфорса (для логина и регистрации) +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 минут + max: 5, // максимум 5 попыток с одного IP + standardHeaders: true, + legacyHeaders: false, + message: "Слишком много попыток входа. Попробуйте позже.", + skipSuccessfulRequests: true, // не считаем успешные попытки +}); + +// Rate limiter для API запросов (более мягкий для обычных операций) +const apiLimiter = rateLimit({ + windowMs: 5 * 60 * 1000, // 5 минут + max: 2000, // максимум 2000 запросов за 5 минут (400 в минуту) + standardHeaders: true, + legacyHeaders: false, + message: "Слишком много запросов к API. Пожалуйста, попробуйте позже.", + skip: (req) => { + // Пропускаем GET запросы (безопасные, не требуют защиты) + return req.method === "GET"; + }, +}); // Статические файлы app.use(express.static(path.join(__dirname, "public"))); @@ -218,6 +281,9 @@ app.get("/browserconfig.xml", (req, res) => { app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); +// Rate limiter для API (применяется ко всем API запросам) +app.use("/api/", apiLimiter); + // Настройка сессий с хранением в SQLite app.use( session({ @@ -230,7 +296,9 @@ app.use( resave: false, saveUninitialized: false, cookie: { - secure: false, // в продакшене установить true с HTTPS + secure: process.env.NODE_ENV === "production", // куки только через HTTPS в продакшене + httpOnly: true, // куки недоступны для JavaScript (защита от XSS) + sameSite: "strict", // защита от CSRF атак maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней }, }) @@ -785,7 +853,7 @@ app.get("/register", (req, res) => { }); // API регистрации -app.post("/api/register", async (req, res) => { +app.post("/api/register", authLimiter, async (req, res) => { const { username, password, confirmPassword } = req.body; // Валидация @@ -841,7 +909,7 @@ app.post("/api/register", async (req, res) => { }); // API входа -app.post("/api/login", async (req, res) => { +app.post("/api/login", authLimiter, async (req, res) => { const { username, password } = req.body; if (!username || !password) { @@ -899,7 +967,7 @@ app.post("/api/login", async (req, res) => { }); // Обработка входа (старый маршрут для совместимости) -app.post("/login", async (req, res) => { +app.post("/login", authLimiter, async (req, res) => { const { password } = req.body; const correctPassword = process.env.APP_PASSWORD; @@ -2961,7 +3029,7 @@ app.post("/api/user/2fa/disable", requireApiAuth, async (req, res) => { }); // API для проверки 2FA кода при входе -app.post("/api/user/2fa/verify", async (req, res) => { +app.post("/api/user/2fa/verify", authLimiter, async (req, res) => { const { username, token } = req.body; if (!username || !token) {