feat: helmet (security headers) + rate-limit (30/min mutations, 20/min export)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const { rateLimit } = require('express-rate-limit');
|
||||
require('dotenv').config();
|
||||
const { checkConnection } = require('./src/db');
|
||||
const { initAuth, requireAdmin } = require('./src/auth');
|
||||
@@ -12,6 +14,32 @@ const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
// ── Безопасность HTTP-заголовков ───────────────────────────────────────────────
|
||||
// helmet выставляет: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security,
|
||||
// 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());
|
||||
@@ -50,7 +78,8 @@ async function start() {
|
||||
// /export — агрегированный список всех активных CIDR по всем компаниям.
|
||||
// ТЗ: «авторизация не требуется, доступ ограничивается на сетевом уровне».
|
||||
// Маршрут регистрируется ДО auth.middleware чтобы не требовать jwt-cookie.
|
||||
app.get('/export', async (req, res) => {
|
||||
// exportLimiter: не более 20 запросов/мин с одного IP (защита от scraping).
|
||||
app.get('/export', exportLimiter, async (req, res) => {
|
||||
try {
|
||||
// Получаем все активные CIDR без фильтра по компании
|
||||
const cidrs = await q.getExportCIDRs();
|
||||
@@ -148,7 +177,8 @@ async function start() {
|
||||
});
|
||||
|
||||
// ── Добавление записи ─────────────────────────────────────────────────────────
|
||||
app.post('/add', async (req, res) => {
|
||||
// mutationLimiter: не более 30 POST-запросов/мин с одного IP.
|
||||
app.post('/add', mutationLimiter, async (req, res) => {
|
||||
const { value, comment } = req.body;
|
||||
const { clientId, companyName, email, isAdmin } = req.user;
|
||||
|
||||
@@ -181,7 +211,7 @@ async function start() {
|
||||
});
|
||||
|
||||
// ── Редактирование записи ─────────────────────────────────────────────────────
|
||||
app.post('/edit/:id', async (req, res) => {
|
||||
app.post('/edit/:id', mutationLimiter, async (req, res) => {
|
||||
const { value, comment } = req.body;
|
||||
const { clientId, companyName, email, isAdmin } = req.user;
|
||||
const entryId = parseInt(req.params.id, 10);
|
||||
@@ -212,7 +242,7 @@ async function start() {
|
||||
});
|
||||
|
||||
// ── Удаление записи (soft delete) ────────────────────────────────────────────
|
||||
app.post('/delete/:id', async (req, res) => {
|
||||
app.post('/delete/:id', mutationLimiter, async (req, res) => {
|
||||
const { clientId, companyName, email, isAdmin } = req.user;
|
||||
const entryId = parseInt(req.params.id, 10);
|
||||
|
||||
@@ -279,7 +309,7 @@ async function start() {
|
||||
|
||||
// ── Изменение лимита компании (только admin) ──────────────────────────────────
|
||||
// ТЗ 4.5: «администратор может задать индивидуальный лимит»
|
||||
app.post('/admin/limit/:companyId', requireAdmin, async (req, res) => {
|
||||
app.post('/admin/limit/:companyId', requireAdmin, mutationLimiter, async (req, res) => {
|
||||
const companyId = parseInt(req.params.companyId, 10);
|
||||
if (!Number.isFinite(companyId) || companyId <= 0) {
|
||||
return res.redirect('/admin?error=' + encodeURIComponent('Некорректный ID компании'));
|
||||
|
||||
Reference in New Issue
Block a user