Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Які основні практики безпеки в Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Практики безпеки в Node.js** включають валідацію вхідних даних, захист від ін'єкцій, хешування паролів і аудит залежностей. Три рядки захищають більшість маршрутів: `helmet()` для заголовків безпеки, `express.json({ limit: '10kb' })` проти великих payloads і `rateLimit()` проти брутфорсу. Валідуй на межі; ніколи не довіряй сирому `req.body`. **Ключовий принцип:** санітизуй вхід першим, запускай від непривілейованого користувача, аудитуй залежності у CI.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Практики безпеки в Node.js** - набір захисних патернів, які блокують найпоширеніші вектори атак: ін'єкції, prototype pollution (забруднення прототипу), брутфорс та експлойти через залежності. ## Теорія ### TL;DR - Node виконує JS на сервері, тому несанітизований вхід може запустити код напряму, а не просто зламати UI - Валідуй кожен вхід на межі, хешуй паролі через bcrypt (12+ раундів) і запускай `npm audit` перед кожним деплоєм - `helmet()` встановлює 7+ security-заголовків одним рядком; без нього XSS, clickjacking і MIME-sniffing відкриті - Prototype pollution через `__proto__` мутує `Object.prototype` глобально і залишається до перезапуску процесу - Ніколи не запускай від root, обмежуй розмір тіла запиту і rate-limit кожен публічний ендпоінт ### Швидкий приклад ```js // Express signup з трьома шарами захисту const helmet = require('helmet'); const { body, validationResult } = require('express-validator'); const rateLimit = require('express-rate-limit'); app.use(helmet()); // CSP, HSTS, X-Frame-Options одним викликом app.use(express.json({ limit: '10kb' })); // блокує надмірно великі payloads const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }); app.post('/signup', limiter, [ body('email').isEmail().normalizeEmail(), body('password').isStrongPassword({ minLength: 12, minNumbers: 1 }) ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors }); // "admin' OR 1=1" не проходить isEmail(); брутфорс впирається в rate limit }); ``` Три рядки до обробника маршруту роблять більшу частину роботи. Middleware валідації запускається до будь-якого звернення до бази, тому некоректний вхід ніколи не доходить до рівня запиту. ### Чому Node.js вимагає серверної валідації Браузери мають CSP і sandbox. У Node цього немає. Несанітизований рядок з `req.body` може потрапити прямо в `eval()`, `exec()` або оператор MongoDB. Event loop обробляє кожен запит синхронно, тому один regex з катастрофічним backtracking-ом може заморозити весь процес для всіх одночасних користувачів. Один запит. Неправильний вхід. Процес зависає. ### Ін'єкції: три варіанти **SQL-ін'єкція** - використовуй параметризовані запити, ніколи конкатенацію рядків. ```js // Погано - зловмисник надсилає id = "' OR '1'='1" const query = `SELECT * FROM users WHERE id = '${req.params.id}'`; // Добре const result = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]); ``` **NoSQL-ін'єкція (MongoDB)** - `{ "password": { "$gt": "" } }` обходить перевірку пароля, якщо передати сирий `req.body` у запит. Приводь до рядка або використовуй схему валідації перед запитом. ```js // Погано - зловмисник надсилає { "password": { "$gt": "" } } db.users.find({ username: req.body.username, password: req.body.password }); // Добре - приведення до рядка і хешування db.users.find({ username: String(req.body.username), password: await bcrypt.hash(req.body.password, 12) }); ``` **Command injection** - `exec()` передає перший аргумент прямо в shell. `execFile()` з масивом аргументів не викликає shell взагалі. ```js // Погано - dir = "; cat /etc/passwd" спрацює без помилки exec(`ls ${req.query.dir}`); // Добре - без shell, аргументи масиву не інтерполюються execFile('ls', [req.query.dir], { encoding: 'utf8' }); ``` ### Prototype pollution (забруднення прототипу) V8 парсить JSON-вхід як звичайні об'єкти. Зловмисник, який надсилає `{ "__proto__": { "isAdmin": true } }`, мутує `Object.prototype` глобально. Кожен об'єкт у процесі успадкує цю властивість до перезапуску. Саме так працювала CVE-2019-10744 проти lodash нижче 4.17.21 у production-додатках. ```js // Блокуємо три небезпечні ключі function safeMerge(target, source) { if (typeof source !== 'object' || source === null) return target; for (const key of Object.keys(source)) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; target[key] = typeof source[key] === 'object' ? safeMerge(target[key] || {}, source[key]) : source[key]; } return target; } // Ще надійніше: Object.create(null) не має ланцюжка прототипів req.user = safeMerge(Object.create(null), req.body.userData || {}); ``` З власного досвіду: prototype pollution - це саме та вразливість, яка найчастіше застає команди зненацька. Мутація глобальна і тиха, жодної помилки немає, тому баг проявляється далеко від місця атаки. ### Аутентифікація та зберігання паролів bcrypt з 12+ раундами солі - це стандарт. MD5 і SHA-1 для паролів не підходять. ```js const bcrypt = require('bcrypt'); const hash = await bcrypt.hash(password, 12); const isValid = await bcrypt.compare(inputPassword, hash); ``` JWT-токени: короткий термін дії (15 хвилин для access-токена), httpOnly cookies замість localStorage, refresh-токени в Redis зі списком відкликання. RS256 замість HS256 дозволяє ротувати signing key без перерозгортання всіх сервісів, бо ключ верифікації публічний. ### Безпека залежностей Запускай `npm audit --audit-level=high` у CI. Одна застаріла транзитивна залежність може відкрити весь додаток. Snyk знаходить більше, ніж `npm audit`, особливо в транзитивних залежностях. ```bash npm audit --audit-level=high npm ci --only=production # dev-залежності не потрапляють у прод ``` За даними Snyk, 70% звітів про вразливості в Node.js пов'язані з dev-залежностями, які потрапили в production. Встанови `production=true` в `.npmrc` або використовуй `npm ci --only=production`. ### Захист від DoS Rate limiting - перша лінія. Обмеження розміру тіла - друга. ReDoS (backtracking у регулярних виразах) - непомітна третя. ```js // 5 спроб входу за 15 хвилин з однієї IP const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }); app.use('/login', loginLimiter); // Ліміт розміру до будь-якого парсингу app.use(express.json({ limit: '10kb' })); // Цей regex зависає процес на "aaaaaaaaaaaaaX" const dangerous = /^(a+)+$/; // re2 гарантує лінійний час виконання const RE2 = require('re2'); const safe = new RE2('^[a-z]+$'); ``` ### Змінні середовища та секрети ```js // Ніколи const secret = 'hardcoded-jwt-secret'; // Завжди const secret = process.env.JWT_SECRET; if (!process.env.JWT_SECRET) throw new Error('JWT_SECRET є обов\'язковим'); ``` Ніколи не коміть `.env` у git. Ротуй секрети, коли розробник виходить з команди. Для production використовуй менеджер секретів (AWS Secrets Manager, HashiCorp Vault). ### Типові помилки **1. `JSON.parse(req.body)` без схемної валідації.** Дозволяє `{ "constructor": { "prototype": { "isAdmin": true } } }` пошкодити `Object.prototype` глобально. Виправлення: валідуй через Joi або express-validator перед роботою з розпарсеним об'єктом. **2. Шаблонні рядки всередині `exec()`.** ```js // dir = "; rm -rf /" спрацює без помилки exec(`ls ${dir}`); // Виправлення: без shell, без інтерполяції execFile('ls', [dir]); ``` **3. Запуск від root.** `app.listen(3000)` від root означає, що при успішному RCE зловмисник отримує root-доступ до машини. Виправлення: `sudo -u www-data node app.js` або `process.setuid('nobody')` перед listen. **4. Dev-залежності в production.** `npm install` у production включає `jest`, `nodemon` та всі їхні дерева залежностей. Кожна - потенційна поверхня атаки. Використовуй `npm ci --only=production`. **5. Відсутній ліміт розміру тіла.** `express.json()` без `limit` приймає payloads будь-якого розміру. JSON на 500 МБ парситься синхронно і виснажує пам'ять. Додай `{ limit: '10kb' }` або відповідний розмір для твого випадку. ### Де це зустрічається в реальних проектах - **Netflix, PayPal API** - `app.use(helmet()); app.use(express.json({ limit: '10kb' }))` першим middleware - **NestJS (мікросервіси Uber)** - `@UseGuards(RateLimitGuard)` разом з декораторами class-validator - **Hapi (e-commerce Walmart)** - `server.auth.strategy('jwt', 'jwt', { key: privateKey })` - **Express-сесії (Discord-боти)** - `express-session({ store: new RedisStore(), secret: process.env.SESSION_SECRET })` - **Fastify (Vercel edge)** - `fastify.register(require('@fastify/rate-limit'))` ### Питання для співбесіди **Q:** Як prototype pollution поширюється між модулями в Node.js? **A:** Мутує `Object.prototype` через шлях парсингу JSON. Кожен цикл `for...in` і виклик `Object.keys()` у кожному модулі бачить забруднену властивість до перезапуску процесу. `Object.freeze(Object.prototype)` зупиняє нові мутації, але зазвичай надто агресивний для production. **Q:** Яка різниця між bcrypt і scrypt для хешування паролів? **A:** bcrypt добре працює на багатоядерному Node (підходить для worker threads); scrypt вимагає більше пам'яті на операцію, що краще захищає від GPU-зламування. Обидва прийнятні. scrypt - новіша рекомендація NIST, якщо починаєш проект з нуля. **Q:** Як захистити завантаження файлів в Express? **A:** Multer з лімітом `fileSize` і фільтром MIME-типів: `fileFilter: (req, file, cb) => /^image\//.test(file.mimetype) ? cb(null, true) : cb(new Error('Invalid type'))`. Скануй файли через ClamAV (`clamscan`) перед переміщенням у постійне сховище. **Q:** Як запобігти ReDoS без переписування кожного regex у кодовій базі? **A:** Використовуй пакет `safe-regex` для виявлення вразливих патернів у CI, потім заміни їх бібліотекою `re2`, яка гарантує лінійний час виконання незалежно від вхідних даних. **Q:** Як інтегрувати аудит залежностей у CI/CD? **A:** `npm audit --audit-level=high` завершується з кодом 1 на вразливостях рівня high або critical. У GitHub Actions: `npm audit --json | jq '.metadata.vulnerabilities.high > 0'` і fail build. Snyk і Sonatype дають глибший аналіз транзитивних залежностей. ## Приклади ### Базовий: Helmet і rate limiting на публічному API ```js const express = require('express'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const app = express(); app.use(helmet()); // X-Content-Type-Options, HSTS, CSP, X-Frame-Options та інші app.use(express.json({ limit: '10kb' })); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: { error: 'Too many requests' } }); app.use('/api/', apiLimiter); // Кожен маршрут /api/* тепер має сім заголовків безпеки і rate limit ``` `helmet()` без аргументів активує сім заголовків, що покривають найпоширеніші браузерні атаки. Ліміт тіла зупиняє виснаження пам'яті до запуску будь-якого контролера. ### Середній: Повна валідація вводу при реєстрації (production-патерн) ```js const { body, validationResult } = require('express-validator'); const bcrypt = require('bcrypt'); app.post('/signup', [ body('email').isEmail().normalizeEmail(), body('password').isStrongPassword({ minLength: 12, minNumbers: 1, minSymbols: 1 }), body('username').trim().escape().isLength({ min: 3, max: 20 }) ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); const hash = await bcrypt.hash(req.body.password, 12); await db.users.create({ email: req.body.email, username: req.body.username, password: hash }); res.status(201).json({ message: 'Created' }); // "admin' OR 1=1" не пройде isEmail(); "password" не пройде isStrongPassword() }); ``` Ланцюжок валідації запускається до тіла async-функції. Якщо будь-яке поле не проходить, запит завершується на шостому рядку. База даних ніколи не бачить сирий вхід користувача. ### Просунутий: Захист від prototype pollution у merge middleware ```js // Вектор атаки: { "userData": { "__proto__": { "isAdmin": true } } } // Використовувався проти lodash < 4.17.21 у production (CVE-2019-10744) function safeMerge(target, source) { if (typeof source !== 'object' || source === null) return target; for (const key of Object.keys(source)) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; target[key] = typeof source[key] === 'object' ? safeMerge(target[key] || {}, source[key]) : source[key]; } return target; } app.use(express.json()); app.use((req, res, next) => { // Object.create(null) не має прототипу - нічого забруднити req.user = safeMerge(Object.create(null), req.body.userData || {}); next(); }); // Після спроби атаки: console.log({}.isAdmin); // undefined - Object.prototype чистий console.log(req.user.isAdmin); // undefined - заблоковано перевіркою ключа ``` `Object.create(null)` створює об'єкт без ланцюжка прототипів. Навіть якщо блок-лист пропустить якийсь ключ, успадковувати нема від чого. Разом з цим запускай `npm audit fix --force` для lodash нижче 4.17.21, щоб закрити базову вразливість залежності.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.