Увеличены rate limits для улучшения производительности
- Основной лимит: 100 → 1000 запросов за 15 минут - API лимит: 500 → 2000 запросов за 5 минут
This commit is contained in:
parent
daa1c14d11
commit
7d115e1845
@ -8,6 +8,7 @@ const session = require("express-session");
|
|||||||
const SQLiteStore = require("connect-sqlite3")(session);
|
const SQLiteStore = require("connect-sqlite3")(session);
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
|
const cors = require("cors");
|
||||||
const rateLimit = require("express-rate-limit");
|
const rateLimit = require("express-rate-limit");
|
||||||
const bodyParser = require("body-parser");
|
const bodyParser = require("body-parser");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
@ -25,6 +26,38 @@ const PORT = process.env.PORT || 3001;
|
|||||||
// Доверяем всем прокси (nginx proxy manager должен передавать X-Forwarded-For)
|
// Доверяем всем прокси (nginx proxy manager должен передавать X-Forwarded-For)
|
||||||
app.set("trust proxy", true);
|
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");
|
const uploadsDir = path.join(__dirname, "public", "uploads");
|
||||||
if (!fs.existsSync(uploadsDir)) {
|
if (!fs.existsSync(uploadsDir)) {
|
||||||
@ -185,12 +218,42 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ограничение запросов (отключено для разработки)
|
// Ограничение запросов (включено для защиты от DDoS и брутфорса)
|
||||||
// const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
// windowMs: 15 * 60 * 1000, // 15 минут
|
windowMs: 15 * 60 * 1000, // 15 минут
|
||||||
// max: 100, // максимум 100 запросов с одного IP
|
max: 1000, // максимум 1000 запросов с одного IP
|
||||||
// });
|
standardHeaders: true, // вернёт информацию о rate limit в `RateLimit-*` заголовки
|
||||||
// app.use(limiter);
|
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")));
|
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.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
// Rate limiter для API (применяется ко всем API запросам)
|
||||||
|
app.use("/api/", apiLimiter);
|
||||||
|
|
||||||
// Настройка сессий с хранением в SQLite
|
// Настройка сессий с хранением в SQLite
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
@ -230,7 +296,9 @@ app.use(
|
|||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: {
|
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 дней
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -785,7 +853,7 @@ app.get("/register", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API регистрации
|
// API регистрации
|
||||||
app.post("/api/register", async (req, res) => {
|
app.post("/api/register", authLimiter, async (req, res) => {
|
||||||
const { username, password, confirmPassword } = req.body;
|
const { username, password, confirmPassword } = req.body;
|
||||||
|
|
||||||
// Валидация
|
// Валидация
|
||||||
@ -841,7 +909,7 @@ app.post("/api/register", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API входа
|
// API входа
|
||||||
app.post("/api/login", async (req, res) => {
|
app.post("/api/login", authLimiter, async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
if (!username || !password) {
|
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 { password } = req.body;
|
||||||
const correctPassword = process.env.APP_PASSWORD;
|
const correctPassword = process.env.APP_PASSWORD;
|
||||||
|
|
||||||
@ -2961,7 +3029,7 @@ app.post("/api/user/2fa/disable", requireApiAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API для проверки 2FA кода при входе
|
// 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;
|
const { username, token } = req.body;
|
||||||
|
|
||||||
if (!username || !token) {
|
if (!username || !token) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user