Головна / Статті / Зміцнення безпеки 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-контейнера, то будь-яка вразливість до виконання коду з відстані дає зловмиснику доступ на рівні root — що може дозволити йому отримати доступ до файлової системи контейнера або навіть до хоста.

Найкращі практики створення 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: позбавляє процес, що виконується, привілеїв root та замість цього запускає його як користувача без привілеїв
  • Багатоетапна компіляція: не включає інструменти розробки, компілятори та набори тестів до кінцевого образу, що зменшує можливості для експлуатації з боку зловмисників
  • npm ci: гарантує відтворювані процеси інсталяції шляхом суворого дотримання точних версій, зазначених у package-lock.json
  • Чек-лист архітектури для частини 4

    Використовуйте цей чек-лист для перевірки розгортання в продакшен перед його випуском:

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

    Пов’язана література