Node.js: безопасность (Helmet, rate-limit)
Helmet и express-rate-limit — базовые middleware для защиты Express-приложений: Helmet устанавливает HTTP-заголовки безопасности, rate-limit ограничивает количество запросов с одного IP.
Зачем нужно
По умолчанию Express не устанавливает заголовки безопасности и не ограничивает запросы. Без Helmet браузер не получает инструкций по CSP, X-Frame-Options, HSTS — открывая XSS, clickjacking и другие атаки. Без rate-limiting API уязвим к brute-force атакам на логин и DDoS-нагрузке. Эти две библиотеки закрывают наиболее распространённые проблемы за несколько строк.
Где используется
- Любой публичный REST API или веб-приложение на Express
- Эндпоинты аутентификации (
/login,/register) — особо строгий rate-limit - Защита от Clickjacking (iframe) — X-Frame-Options
- HTTPS-only production — HSTS заголовок через Helmet
Основной контент
Helmet — заголовки безопасности
npm install helmet
const express = require('express');
const helmet = require('helmet');
const app = express;
// Подключить все заголовки безопасности (рекомендуется)
app.use(helmet);
// Что helmet устанавливает по умолчанию:
// Content-Security-Policy: default-src 'self'
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN
// X-XSS-Protection: 0 (устарел, CSP лучше)
// Strict-Transport-Security: max-age=15552000
// Referrer-Policy: no-referrer
// X-DNS-Prefetch-Control: off
// X-Permitted-Cross-Domain-Policies: none
// Cross-Origin-Opener-Policy: same-origin
// Тонкая настройка
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.example.com'],
imgSrc: ["'self'", 'data:', 'https:'],
}
},
hsts: {
maxAge: 31536000, // 1 год
includeSubDomains: true,
preload: true
},
frameguard: { action: 'deny' } // запретить iframe полностью
}));
express-rate-limit
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Глобальный лимит: 100 запросов за 15 минут с одного IP
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100,
standardHeaders: true, // Rate-Limit заголовки в ответе
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' }
});
app.use(globalLimiter);
// Строгий лимит для аутентификации: 5 попыток за 15 минут
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // не считать успешные логины
message: { error: 'Too many login attempts, try again in 15 minutes.' }
});
app.use('/api/auth', authLimiter);
CORS — Cross-Origin Resource Sharing
npm install cors
const cors = require('cors');
// Разрешить конкретный origin
app.use(cors({
origin: ['https://myapp.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // разрешить куки
}));
// Или динамически из whitelist
const whitelist = ['https://myapp.com'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || whitelist.includes(origin)) callback(null, true);
else callback(new Error('Not allowed by CORS'));
}
}));
Полная конфигурация безопасности
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const app = express;
// 1. Заголовки безопасности
app.use(helmet);
// 2. CORS
app.use(cors({ origin: process.env.ALLOWED_ORIGIN }));
// 3. Парсинг с лимитом размера
app.use(express.json({ limit: '10kb' })); // защита от payload flooding
// 4. Rate limiting
app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use('/api/auth/', rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }));
// 5. Отключить заголовок X-Powered-By
app.disable('x-powered-by'); // helmet делает это автоматически
Частые ошибки
- Helmet после маршрутов — middleware должен подключаться до маршрутов, иначе заголовки не установятся
- Отключить CSP для удобства —
contentSecurityPolicy: falseубирает защиту от XSS; лучше настроить директивы - Один rate-limit для всего —
/loginдолжен иметь более строгий лимит, чем/api/products - Не учитывать reverse proxy — за nginx/load balancer
req.ipбудет IP прокси; настроитьapp.set('trust proxy', 1)для правильного IP клиента
Связанные темы
Ресурсы
🎓 Источник: Безопасность приложений Node.js
- 📅 2019-11-28 · YouTube · [Marp](../../Documents/TimurShemsedinov/2019-11-28 — 💻 Безопасность приложений Node.js Security (Pdfo1G-gI6s).md)
- Тезисы:
- MD5 не защищает пароли — нужен bcrypt/argon2/scrypt + соль
- Соль уникальная на каждый сервер + на каждого пользователя
- Общая память между запросами — главная уязвимость Node по сравнению с PHP (где процесс пересоздаётся на запрос). Утечка req-объектов в глобальное состояние = утечка данных одного юзера другому
- Тайпосквоттинг в npm:
expresвместоexpress— заражённый пакет получает полный доступ - Заражённый npm-пакет = доступ к окружению, env, файлам, можно отправить токены наружу
- Смотри ВСЁ дерево зависимостей через
npm ls, не только direct - Выбирай зависимости по коду и активности, а не по звёздам
- SQL-инъекция через конкатенацию строк,
or 1=1,UNION SELECTдля кражи паролей - CSRF через ссылку с GET-параметрами — API никогда не должен принимать команды через GET
- XSS через пользовательский ввод → CSP + HttpOnly cookies
- Path Traversal:
path.join('./files', userInput)не спасает (../), нужен абсолютный путь +startsWithкорня - Кража исходников через path traversal:
/api/files?name=../../config.jsonотдаст ключи throwв обработчике запроса теряет коннекшен — лучше логировать- Отдельный процесс для админ-функций
- Хранение секретов — отдельный сервер (Vault, AWS Secrets Manager)
- Цитата: «Не доверяй ни одной зависимости — заражённый пакет = полный root в твоём приложении»