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 в твоём приложении»