Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як захистити додаток Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Безпека Express.js** будується з кількох middleware-шарів, кожен з яких зупиняє окремий клас атак. ```js app.use(helmet()); // встановлює 11 HTTP-заголовків безпеки app.use(rateLimit({ windowMs: 900000, max: 100 })); // блокує brute-force app.use(mongoSanitize()); // видаляє оператори NoSQL-ін'єкцій app.use(express.json({ limit: '10kb' })); // запобігає вичерпанню пам'яті ``` **Головне:** порядок middleware має значення. Шари безпеки підключаються до маршрутів, а секрети зберігаються в змінних середовища, ніколи в коді.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Безпека Express.js** - це набір middleware-шарів, які захищають API від поширених веб-атак. Кожен шар зупиняє окремий клас загроз, і всі вони потрібні разом. ## Теорія ### TL;DR - `helmet` автоматично встановлює 11 HTTP-заголовків безпеки, включно з CSP, HSTS і X-Frame-Options - Rate limiting блокує brute-force на маршрутах авторизації (5 спроб за 15 хвилин - стандартний поріг) - Санітизація (sanitization) вводу видаляє оператори Mongo (`$where`, `$gt`) і HTML-теги до того, як вони дістануться твоєї логіки - Секрети зберігаються в змінних середовища, ніколи в коді - Порядок middleware має значення: шари безпеки підключаються до маршрутів ### Базове налаштування ```js const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const mongoSanitize = require('express-mongo-sanitize'); const xss = require('xss-clean'); const hpp = require('hpp'); // Middleware безпеки запускається до будь-яких маршрутів app.use(helmet()); app.use(mongoSanitize()); // видаляє оператори MongoDB-ін'єкцій app.use(xss()); // прибирає HTML/script-теги з тіла запиту app.use(hpp()); // запобігає дублюванню query-параметрів app.use(express.json({ limit: '10kb' })); // обмежує розмір тіла запиту ``` Один виклик `helmet()` замінює ручне встановлення 11 заголовків. Решта покривають ін'єкцію, XSS, забруднення параметрів і атаки через розмір payload. ### HTTP-заголовки через Helmet Звичайний Express-додаток відправляє `X-Powered-By: Express` у кожній відповіді. Це відразу говорить сканеру, який стек ти використовуєш. `helmet()` прибирає цей заголовок і додає ті, що браузери реально застосовують. Що `helmet()` встановлює за замовчуванням: - `Content-Security-Policy` - контролює, звідки можна завантажувати скрипти та ресурси - `X-Frame-Options: DENY` - блокує clickjacking через iframe - `Strict-Transport-Security` - каже браузерам використовувати тільки HTTPS наступний рік - `X-Content-Type-Options: nosniff` - запобігає атакам через підбір MIME-типу Для більшості додатків ручне налаштування не потрібне. `app.use(helmet())` - достатньо для старту. ### Rate limiting Будь-який ендпоінт, що приймає облікові дані, без rate limiting є мішенню для brute-force. `express-rate-limit` відстежує запити за IP і блокує після досягнення порогу. ```js const rateLimit = require('express-rate-limit'); // Загальне обмеження для API const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // вікно 15 хвилин max: 100, standardHeaders: true, legacyHeaders: false, message: { error: 'Занадто багато запитів, спробуй пізніше' } }); // Суворіше обмеження для маршрутів авторизації const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // 5 спроб за 15 хвилин skipSuccessfulRequests: true, // зараховуються тільки невдалі спроби message: { error: 'Занадто багато спроб входу' } }); app.use('/api/', apiLimiter); app.use('/auth/login', authLimiter); app.use('/auth/register', authLimiter); ``` `skipSuccessfulRequests: true` на маршрутах авторизації означає, що реальний користувач, який кілька разів входить успішно, не заблокується. До ліміту потрапляють лише невдалі спроби. ### Санітизація вводу і розмір payload Два окремих вектори атаки надходять через тіло запиту. NoSQL-ін'єкція і XSS обидва експлуатують несанітизований ввід. Занадто великі payload можуть вичерпати пам'ять і покласти сервер. ```js app.use(mongoSanitize()); // видаляє $where, $gt, $ne та подібні оператори app.use(xss()); // прибирає script/HTML-теги з полів вводу app.use(express.json({ limit: '10kb' })); app.use(express.urlencoded({ limit: '10kb', extended: true })); ``` `10kb` - розумний за замовчуванням для більшості JSON API. Завантаження файлів обробляються окремим middleware на кшталт `multer` з власними обмеженнями розміру. ### Примусовий HTTPS У продакшені весь трафік має бути зашифрований. Якщо Express стоїть за nginx або балансувальником навантаження, TLS-термінація зазвичай відбувається на рівні проксі. Якщо Express відкритий напряму, додай редирект: ```js app.use((req, res, next) => { if (process.env.NODE_ENV === 'production' && !req.secure) { return res.redirect(301, `https://${req.hostname}${req.url}`); } next(); }); ``` `helmet()` додатково встановлює `Strict-Transport-Security`, що каже браузерам пам'ятати про HTTPS навіть якщо користувач вводить `http://` вручну. ### Секрети в змінних середовища ```js // Це потрапить в git-історію. Хтось знайде. const JWT_SECRET = 'hardcoded-secret-123'; // Це залишається на сервері. const JWT_SECRET = process.env.JWT_SECRET; ``` Локально використовуй `dotenv`. У продакшені змінні середовища вводяться через платформу деплою (Railway, Render, AWS ECS). Додай `.env` до `.gitignore` одразу, коли починаєш проєкт, а не після того, як вже щось закомітив. ### Запобігання забрудненню параметрів Маловідома атака: передача дублікатів query-параметрів на кшталт `/api/users?sort=name&sort=admin`. Express та middleware обробляють дублікати по-різному: одні беруть перше значення, інші - останнє, треті будують масив. Поведінка непередбачувана. Пакет `hpp` нормалізує це: ```js const hpp = require('hpp'); app.use(hpp()); // /api?sort=name&sort=price → req.query.sort стає 'price' ``` ### Типові помилки **Неправильний порядок middleware.** Якщо `helmet()` підключений після маршрутів, він не додасть заголовки до їхніх відповідей. Middleware безпеки завжди йде першим, до будь-яких визначень маршрутів. **Великий ліміт розміру тіла.** `limit: '100mb'` у `express.json()` - типове копіювання з туторіалу, що передбачав файлові завантаження. JSON-тіло розміром 100mb може вичерпати RAM сервера за секунди. За замовчуванням встановлюй `10kb` і збільшуй лише при конкретній потребі. **Відсутність `skipSuccessfulRequests` на маршрутах входу.** Без цього користувач, який 100 разів успішно залогінився, заблокується так само, як і зловмисник. Встановлюй `true` на ендпоінтах авторизації. **Відсутність перевірки секретів при запуску.** Запуск з `JWT_SECRET = undefined` означає, що `jsonwebtoken` кине помилку в момент спроби входу, а не при старті. Краще перевіряти одразу: ```js if (!process.env.JWT_SECRET) { console.error('JWT_SECRET не встановлено'); process.exit(1); // падаємо голосно при старті, а не опівночі } ``` **Різні повідомлення про помилку для неправильного email і неправильного пароля.** Це дає зловмиснику змогу перебирати, які акаунти існують. Завжди повертай однакове загальне повідомлення для обох випадків. ### Чеклист безпеки | Шар | Пакет | Що зупиняє | |---|---|---| | HTTP-заголовки | `helmet` | XSS, clickjacking, MIME-снифінг | | Rate limiting | `express-rate-limit` | Brute-force, credential stuffing | | CORS | `cors` | Неавторизовані cross-origin запити | | NoSQL-ін'єкція | `express-mongo-sanitize` | Ін'єкція операторів MongoDB | | XSS | `xss-clean` | Ін'єкція скриптів через тіло запиту | | Розмір payload | `express.json({ limit })` | Вичерпання пам'яті | | JWT | `jsonwebtoken` | Підробка сесії | | Хешування паролів | `bcryptjs` (12+ раундів) | Витік облікових даних | | HTTPS | nginx / redirect middleware | Перехоплення трафіку | | Секрети | env vars + `dotenv` | Витік credentials у коді | | Забруднення параметрів | `hpp` | Непередбачуваний парсинг запитів | ### Питання на співбесіді **Q:** Чи працює rate limiting за IP за балансувальником навантаження? **A:** Ні, за замовчуванням не працює. Якщо весь трафік приходить з одного IP балансувальника, всі користувачі ділять один лічильник. Потрібно встановити `trustProxy: true` і читати `X-Forwarded-For` для реального IP клієнта. При кількох інстансах сервера - підключи Redis-сховище, щоб усі інстанси ділили спільний стан rate limit. **Q:** Чи достатньо `xss-clean` для захисту від XSS? **A:** Ні. `xss-clean` санітизує тіло запиту на вході. XSS спрацьовує, коли несанітизовані дані виводяться в HTML на виході. Потрібне також екранування на фронтенді і суворий `Content-Security-Policy` через `helmet`. `xss-clean` - це один шар, не повне рішення. **Q:** Коли варто використовувати більше ніж 12 раундів у `bcrypt`? **A:** Тільки якщо сервер витримає CPU-навантаження. Заміряй на реальному залізі. Хеш, що займає 200-300ms на операцію, зазвичай є правильним компромісом. Більше - і затримка при вході стає помітною для користувачів. **Q:** Що робити, якщо багато користувачів за одним IP (корпоративний NAT, університет)? **A:** Rate limiting тільки за IP заблокує всіх за одним NAT після того, як один порушник досягне ліміту. Комбінуй з блокуванням на рівні акаунта (блокуй акаунт після 5 невдалих спроб незалежно від IP) і додавай CAPTCHA при повторних невдачах. **Q:** Навіщо прибирати заголовок `X-Powered-By`? **A:** Сам по собі він мало що захищає. Але він дає зловмиснику сигнал, який звужує вибір CVE та експлоїтів для перевірки. Автоматичні сканери перевіряють мільйони ендпоінтів. Дати їм менше інформації нічого не коштує. `helmet()` прибирає цей заголовок автоматично. ## Приклади ### Базовий стек безпеки для нового Express-додатку ```js require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const mongoSanitize = require('express-mongo-sanitize'); const xss = require('xss-clean'); const hpp = require('hpp'); const app = express(); // Спочатку безпека, потім маршрути app.use(helmet()); app.use(mongoSanitize()); app.use(xss()); app.use(hpp()); app.use(express.json({ limit: '10kb' })); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false }); app.use('/api', apiLimiter); app.get('/api/users', (req, res) => { res.json({ users: [] }); }); app.listen(process.env.PORT || 3000); ``` Увесь middleware безпеки запускається до будь-якого обробника маршруту. Express обробляє middleware зверху вниз, тому порядок не опціональний. ### Маршрут авторизації з суворішим rate limiting і хешуванням паролів ```js const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, skipSuccessfulRequests: true, message: { error: 'Занадто багато спроб входу, спробуй за 15 хвилин' } }); app.post('/auth/login', authLimiter, async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user) { // Однакове повідомлення і для неправильного email, і для неправильного пароля return res.status(401).json({ error: 'Невірні облікові дані' }); } const match = await bcrypt.compare(password, user.passwordHash); if (!match) return res.status(401).json({ error: 'Невірні облікові дані' }); const token = jwt.sign( { userId: user._id }, process.env.JWT_SECRET, { expiresIn: '15m' } // короткий термін; для довших сесій використовуй refresh tokens ); res.json({ token }); }); ``` Обидва випадки - неправильний email і неправильний пароль - повертають однакову загальну помилку. Це блокує перебір акаунтів, коли зловмисник перевіряє, які email-адреси зареєстровані. ### Перевірка секретів при запуску ```js // Перевіряємо всі необхідні секрети при старті, а не в момент запиту const requiredEnv = ['JWT_SECRET', 'DB_PASSWORD', 'SESSION_SECRET']; for (const key of requiredEnv) { if (!process.env[key]) { console.error(`Відсутня обов'язкова змінна середовища: ${key}`); process.exit(1); } } // Примусовий HTTPS у продакшені if (process.env.NODE_ENV === 'production') { app.use((req, res, next) => { if (!req.secure) { return res.redirect(301, `https://${req.hostname}${req.url}`); } next(); }); } ``` Падати при старті через відсутній секрет краще, ніж виявити проблему, коли користувач намагається увійти опівночі. Я бачив додатки, що тижнями працювали з `JWT_SECRET = undefined`, бо помилка з'являлась лише в момент підпису токена, а не при завантаженні.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.