Забезпечэнне безпекі Node.js: захад проты DoS, ін’екцый і захад для контэйнероў
Выучыце практычныя методы для захавання служб Node.js ад атак типу DoS, ReDoS, атак на вбрыск дадзенняў і небезпечных контэйнераў за дапамою лімітавання частоты запытоў і адпаведных інструментаў, такіх як Helmet.
Нават бэкенд з высокім рыгорам тэставання і чыстай архітектурой можа стаць недоступным за мгнуценьне, якщо ён вразлівы да проблем з цыклам выдання задач, атак на ін’екцыю дадзеных чыста да слабка захіщанага рэнтайму контейнера.
Пакалькі 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?
Супаўязаныя матэрыялы
- 20 шаблонавацькіх падчынстваў Node.js, якія запобегаюць перывам у роботе сервера — Дзеянне 20 практычных шаблонавацькіх падчынстваў Node.js — ад карэспакціі памылак да граказныяго выключэння і канектацыйнага пулінгу — якія запобегаюць збоям прычым перзапуску.
- Справака з командамі Node.js для локальнага развіцця і сервераў у прымэтным выкарыстанні — Справака з командамі, якую лёгка прачытаць; ў яй паведамлены вопыты карэнтнага адзначэння версый Node.js, калектароў пакетаў, наладжвання сяродавысці, дэбаггіну, PM2 і развяртання на Linux без перываў у роботе.