Главная / Статьи / Усиление безопасности Node.js: защита от DoS, инъекций и контейнеров

Усиление безопасности Node.js: защита от DoS, инъекций и контейнеров

Изучите практические методы защиты сервисов Node.js от атак типа DoS и ReDoS в цикле событий, атак инъекций, а также от небезопасных контейнеров с использованием ограничения скорости запросов и инструмента Helmet.

1457 слов

Даже бэкенд с отличным уровнем тестирования и чистой архитектурой может мгновенно перестать работать, если он уязвим к проблемам с циклом событий, атакам инъекций или слабо защищенной среде выполнения контейнеров.

Поскольку Node.js работает на однопоточном цикле событий, проблемы безопасности в сервисах на Node.js проявляются не только в утечках данных, но и в полной недоступности — полном отказе в обслуживании.

1. Защита цикла событий: предотвращение отказа в обслуживании (DoS)

Самым серьезным операционным риском в Node.js является блокировка цикла событий. Если злоумышленник предоставит входные данные, заставляющие движок регулярных выражений выполнять операции с сложностью O(2^n), или перегрузит сервер чрезмерно большими телами HTTP-запросов, цикл событий остановится. Как только он замрет, процесс перестанет отвечать на все запросы от всех клиентов, а не только от злонамеренных.

Incoming Request Floods ──┐
                          ▼
             ┌──────────────────────────┐
             │ Node.js Event Loop       │
             │ (Single Thread Execution)│
             └────────────┬─────────────┘
                          │ ❌ Synchronous CPU Block
                          ▼
             ┌──────────────────────────┐
             │ Event Loop Freeze        │ ──► All other concurrent requests
             │ (High Latency / DoS)     │     time out or drop!
             └──────────────────────────┘

Защита А: строгие ограничения на тело запроса и полезную нагрузку

При использовании стандартных настроек многие промежуточные компоненты для обработки тела запроса с радостью принимают данные объемом, достаточным для исчерпания доступной памяти или значительного потребления процессорных ресурсов только на их обработку.

Неправильный подход: неограниченная обработка тела запроса

// ❌ BAD Accepts arbitrarily large JSON payloads
app.use(express.json());

Правильный подход: установление ограничений на полезную нагрузку

Укажите четкие пределы размера как на уровне приложения, так и на уровне входных точек — в обратном прокси или слое входного трафика, таком как NGINX, Cloudflare или AWS Application Load Balancer.

// shared/middleware/security.js
const express = require('express');

// Strict payload limits per content type
const configurePayloadLimits = (app) => {
  // Limit standard JSON bodies to 100kb
  app.use(express.json({ limit: '100kb' }));

  // Limit URL-encoded forms to 50kb
  app.use(express.urlencoded({ extended: true, limit: '50kb' }));
};

module.exports = { configurePayloadLimits };

Защита Б: предотвращение атак типа ReDoS (отказ в обслуживании с использованием регулярных выражений)

Уязвимость ReDoS возникает, когда ваш код использует регулярные выражения с катастрофическим обратным поиском для обработки данных, контролируемых злоумышленником.

Уязвимый шаблон: вложенные квантификаторы

// ❌ VULNERABLE: Exponential backtracking O(2^n)
const emailRegex = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;

// An input like "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" will hang the process for minutes!
emailRegex.test(userInput);

Безопасный подход: использование проверенных библиотек или линейных движков

Вместо ручной разработки регулярных выражений для сложной логики проверки лучше использовать хорошо протестированные библиотеки схем, такие как Zod или Joi, либо специализированные пакеты для проверки, например validator.

const { z } = require('zod');

// Schema validation using safe built-in string parsers
const UserInputSchema = z.object({
  email: z.string().email().max(255), // Enforces linear-time length constraints
  username: z.string().alphanumeric().min(3).max(30)
});

function validatePayload(payload) {
  return UserInputSchema.parse(payload);
}

2. Предотвращение атак типа инъекции (SQL, NoSQL и команды)

Проблемы с инъекциями возникают каждый раз, когда ненадежные значения вставляются напрямую в запросы к базе данных или в команды, передаваемые операционной системе.

Неправильный способ: объединение строк в запросах

// ❌ VULNERABLE: Direct string interpolation (SQL Injection)
async function findUserByEmail(email) {
  const query = `SELECT * FROM users WHERE email = '${email}'`;
  return await db.query(query); // Attacker passes: "' OR '1'='1"
}

Правильный способ: параметризованные запросы и строгая типизация

Избегайте формирования запросов путем вставки необработанных параметров непосредственно в вызовы драйвера базы данных или ORM. Используйте местоимения параметров — $1, $2 или ? в зависимости от драйвера — чтобы движок базы данных всегда рассматривал переданные значения как чистые данные, а не как исполняемый SQL-код.

// ✅ GOOD: Parameterized SQL Execution
async function findUserByEmail(email) {
  const text = 'SELECT id, email, role, created_at FROM users WHERE email = $1';
  const values = [email];

  const result = await db.query(text, values);
  return result.rows[0] || null;
}

3. Распределенное ограничение скорости через Redis

Ограничение скорости — это не опциональная функция для серьезного API. Если его пренебречь, это откроет путь для злоупотребляющих клиентов или некорректно работающих интеграций, которые будут агрессивно обращаться к вашим конечным точкам, исчерпывая соединения с базой данных и снижая производительность для всех остальных.

Проблема заключается в том, что в средах с несколькими запущенными инстансами — например, Kubernetes-подами за балансировщиком нагрузки — лимитатор скорости, хранящий свои счетчики в локальной памяти, просто не работает, поскольку у каждого процесса есть собственное изолированное состояние. Вместо этого требуется общий распределенный хранилище, такое как Redis, чтобы каждая инстанс могла проверять и обновлять один и тот же атомарный счетчик.

Client Requests ──► [ Pod 1 ] ──┐
                                ├──► [ Distributed Redis ]
Client Requests ──► [ Pod 2 ] ──┘    (Atomic Request Counter)

Реализация лимитатора скорости для корпоративных приложений

// shared/middleware/rateLimiter.js
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = require('../database/redis');

// Create rate limiter instance: max 100 requests per 15 minutes per IP
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'rl_api',
  points: 100, // Maximum requests allowed
  duration: 15 * 60, // Per 15 minutes (in seconds)
  blockDuration: 60 * 5 // Block IP for 5 minutes if exceeded
});

const rateLimiterMiddleware = async (req, res, next) => {
  try {
    const clientIp = req.ip || req.headers['x-forwarded-for'];
    await rateLimiter.consume(clientIp);
    next();
  } catch (rejRes) {
    // Return standard HTTP 429 Too Many Requests
    res.setHeader('Retry-After', Math.round(rejRes.msBeforeNext / 1000) || 60);
    return res.status(429).json({
      status: 'error',
      code: 'TOO_MANY_REQUESTS',
      message: 'Rate limit exceeded. Please slow down your requests.'
    });
  }
};

module.exports = rateLimiterMiddleware;

4. Безопасность HTTP-заголовков с использованием Helmet

По умолчанию ответы Express раскрывают внутренние детали через такие заголовки, как X-Powered-By: Express. Этот один заголовок дает злоумышленникам удобный путь: они точно знают, какой фреймворк вы используете, и могут сразу обратиться к известным уязвимостям, специфичным для данного фреймворка.

Решение заключается в использовании усиленного набора стандартных заголовков ответа с помощью промежуточного компонента helmet.

// shared/middleware/securityHeaders.js
const helmet = require('helmet');

function applySecurityHeaders(app) {
  app.use(
    helmet({
      // Hide frame options to mitigate Clickjacking
      frameguard: { action: 'deny' },
      // Strict Content Security Policy
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          scriptSrc: ["'self'"],
          objectSrc: ["'none'"],
          upgradeInsecureRequests: []
        }
      },
      // Remove X-Powered-By header
      hidePoweredBy: true,
      // Enforce HTTPS
      hsts: {
        maxAge: 31536000, // 1 year
        includeSubDomains: true,
        preload: true
      }
    })
  );
}

module.exports = { applySecurityHeaders };

5. Безопасность контейнеров и среды выполнения: усиление Dockerfiles

Недостаточно писать безопасный код приложения, если процесс развертывания нарушает эти меры безопасности. Если процесс Node.js запускается от имени root внутри своего Docker-контейнера, любая уязвимость, позволяющая к выполнению удаленного кода, дает злоумышленнику права суперпользователя — что может позволить ему получить доступ к файловой системе контейнера или даже к хост-системе.

Лучшие практики создания Dockerfiles для производственной среды с несколькими этапами

# -------------------------------------------------------------------
# STAGE 1: Build & Dependencies
# -------------------------------------------------------------------
FROM node:20-alpine AS builder

WORKDIR /usr/src/app

# Copy dependency manifests
COPY package*.json ./

# Install all dependencies (including devDependencies for building/testing)
RUN npm ci

# Copy source code
COPY . .

# Prune devDependencies for production runtime
RUN npm prune --production

# -------------------------------------------------------------------
# STAGE 2: Minimal Production Runtime
# -------------------------------------------------------------------
FROM node:20-alpine AS runner

# Set NODE_ENV to production
ENV NODE_ENV=production

WORKDIR /usr/src/app

# Copy built artifacts and production node_modules from builder
COPY --chown=node:node package*.json ./
COPY --chown=node:node --from=builder /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=builder /usr/src/app/src ./src

# 🔒 SECURITY REQUIREMENT: Never run Node.js as root in container environments
USER node

EXPOSE 3000

CMD ["node", "src/server.js"]

Почему это важно:

  • USER node: снимает привилегии суперпользователя у запущенного процесса и заставляет его работать от имени обычного пользователя
  • Многоступенчатая сборка: исключает инструменты разработки, компиляторы и наборы тестов из окончательного образа, сокращая объем того, что может быть использовано злоумышленником
  • npm ci: обеспечивает воспроизводимую установку путем строгого соблюдения точных версий, указанных в package-lock.json
  • Чек-лист архитектуры для части 4

    Используйте этот чек-лист для аудита развертывания в производстве перед его отправкой:

    • Ограничения на данные запроса: Защищен ли каждый конечный пункт API ограничением на размер данных HTTP-запроса, например с помощью express.json({ limit: '100kb' })?
    • Защита от ReDoS: Проверяется ли вся ненадежная строковая информация с помощью проверенных библиотек (Zod, Joi) или логики проверки без обратного прослеживания, а не с помощью пользовательских регулярных выражений?
    • Параметризованные запросы: Обеспечиваются ли все вызовы базы данных передачей вводимых пользователем данных через параметры с ограничениями ($1, $2 или аналоги), чтобы инъекции в SQL и NoSQL были структурно невозможны?
    • Распределенное ограничение частоты запросов: Действует ли атомарный лимитер частоты запросов на основе Redis для защиты ваших публичных API от трафика, направленного на сбой обслуживания?
    • Заголовки безопасности: Настроен ли helmet для подавления заголовков, идентифицирующих фреймворк, обеспечения соблюдения протокола HSTS и блокировки встраивания фреймов?
    • Пользователь контейнера без привилегий: Явно ли в вашем Dockerfile указано USER node, чтобы процесс никогда не выполнялся от имени root?
    • Проверка зависимостей: Включены ли в вашу CI/CD-пайплайн автоматизированные проверки на наличие уязвимостей — npm audit, Snyk, Dependabot или аналогичные инструменты?

    Связанные материалы