Галоўная / Артыкулы / Забезпечэнне безпекі 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!
             └──────────────────────────┘

Захад А: Строгі ліміты для тэлу запытку і паданых дадзенняў

Якщо застаўляць ўшысткія настройкі на значэннях за замовчаннем, багатыя сервісы для парсавання тэлу запытку будуць спакойна прымаць паданыя такога велікага розмеру, што гэта можа выцягнуць усю доступную память або зайняць час на обробку, які перавышае можлівасці CPU.

Неправильны спосаб: Безлімітна обробка тэлу запытку

// ❌ 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 (Denial of Service за дапамою рэгулярных выразаў)

Уязвемасць типу 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, & Command)

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

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

// ❌ 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. Безпека контейнераў і часу выканання: падкрэпленне Dockerfile

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

Найкращыя практыкі стварэння Dockerfile для працы ў кількох стадіях

# -------------------------------------------------------------------
# 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 ад трафіку, які спрычыняе адмову у службах?
    • Заголовкі безпекі: Чы helmet налаштаваны так, каб скасаваць заголовкі, якія ідентыфікуюць фрэймворк, забезпечыць дотрымленьне прынцыпа HSTS і заблокаваць вбудовванне фрэймоў?
    • Корыстнік контейнера без прывілеяў: Чы ваш Dockerfile явна заявляе USER node, так што процес ніколі не запускаецца як root?
    • Аналіз залежнасцяў: Чы автаматызаваныя перагледы вразломнасцей — npm audit, Snyk, Dependabot або падобныя — інтеграваныя ў вашу паўэлку CI/CD?

    Супаўязаныя матэрыялы