Забезпечення безпеки API у Express: автентифікація, перевірка даних, обмеження частоти запитів та моніторинг.
Практичний огляд безпеки API у Express: автентифікація через Passport та JWT, моделі авторизації, шифрування AES-GCM, перевірка даних, обмеження частоти запитів та журналування.
Кожен API, який ви розкриваєте, є дверима до вашої системи, і зловмисники перевіряють ці двері набагато більш систематично, ніж їх тестують більшість команд. Безпека, яку додають після запуску, часто залишає прогалини: маршрут без захисту, запит, створений на основі необробленого вхідного даних, кінцева точка входу, яка охоче приймає мільйон спроб. У цьому посібнику розглядаються стандарти, які варто знати, найпоширеніші загрози та шість конкретних рівнів захисту, реалізованих у Node.js з Express, щоб ви могли пройти аудит існуючого API або створити новий з захистом вже з першого дня.
Розглядайте наведене нижче як чек-лист, до якого ви будете звертатися протягом усього життєвого циклу розробки: перед випуском, після оновлення та щоразу, коли змінюється якась залежність чи маршрут. Регулярне виконання цих перевірок дозволяє виявляти вразливості, поки вони ще невеликі.
Чому API потребують спеціальної уваги до безпеки
Сучасні продукти все частіше створюються за допомогою API. Замість того, щоб розробляти кожну функцію самостійно, команди інтегрують сервіси оплати, ідентифікації, обміну повідомленнями та обробки даних через чітко визначені інтерфейси, і багато компаній тепер випускають продукти, орієнтовані на API. Одне з досліджень ринку оцінює розмір економіки API у 2026 році приблизно у 20 мільярдів доларів (резюме звіту); якою б не була точна цифра, залежність є реальною та постійно зростає.
Ця залежність є двосторонньою. API надає легітимним клієнтам готові, повторно використовувані функції, але водночас створює для зловмисників документований, зручний для машин вхідний пункт. Дослідження галузі постійно пов’язують значну кількість порушень безпеки з API; звіт Traceable за 2023 рік приписує їм 74% випадків витоку даних.
API часто знаходяться безпосередньо біля найбільш конфіденційних даних, якими володіє компанія: платформи ідентифікації з особистими даними, фінансові записи, внутрішні процеси роботи. Несанкціонований доступ може призвести до пошкодження даних, зловживання послугами, фінансових втрат, втрати довіри клієнтів та штрафів згідно із законодавством про захист даних. Оскільки на кону стоять такі високі ризики, безпека має бути передбачена у угодах про рівень послуг разом із часом їх функціонування, і це є відповідальністю кожної команди, що розробляє продукт, а не лише спеціалізованої групи з безпеки. Розумним початком є використання стандартів, які вже узгоджені в галузі.
Стандарти та фреймворки, які варто знати
Стандарти безпеки API — це формальні специфікації, протоколи та рекомендації, які застосовуються протягом усього життєвого циклу розробки програмного забезпечення, щоб захист був послідовним, а не імпровізованим. Найпоширеніші з них:
- OWASP API Security Top 10: ранжований список найбільш критичних ризиків API, який підтримується проектом Open Web Application Security Project. Він охоплює такі проблеми, як несправна авторизація на рівні об’єктів, підробка запитів з боку сервера та необмежене споживання ресурсів, і є найкращою вихідною точкою для аналізу загроз.
- OAuth 2.0 та 2.1: фреймворк делегованої авторизації. Клієнт отримує токен доступу з визначеними обмеженнями та використовує його для дій від імені користувача, не маючи доступу до його пароля; токени оновлення дозволяють клієнту отримувати нові токени доступу без необхідності знову запитувати користувача.
- OpenID Connect (OIDC): шар ідентифікації, створений на основі OAuth. Він стандартизує токен ідентифікації та способи його верифікації клієнтами, що забезпечує сумісність функцій одноразового входу та отримання профілю від постачальників ідентифікації.
Це створює міцну основу, хоча цей список не є вичерпним.
Загрози, від яких ви захищаєтесь
Найпоширеніші вразливості у справжніх API є:
- Порушена автентифікація: слабкі або відсутні перевірки ідентичності та неефективне керування сесіями дозволяють зловмиснику вкрасти куки чи токени та використати їх знову для доступу до ваших сервісів.
- Порушена авторизація на рівні об’єктів (BOLA): API перевіряє, чи у користувача є доступ до системи, але не перевіряє, чи дозволено йому редагувати певний запис, тож зміна ID у URL може викрити дані іншої особи чи внутрішні процеси роботи.
- Вставка SQL: дані, контрольовані зловмисником, додаються до запиту та виконуються базою даних. Майже кожен клієнт бази даних пропонує механізм параметрів, який забезпечує безпечну передачу значень.
- Вставка команд: ненадійні дані з запиту потрапляють до системного шеллу чи процесора команд, що дозволяє зловмиснику виконувати довільні команди з привілеями процесу вашого сервера.
Для кожного з цих аспектів існує відповідна практика програмування. Решта цього посібника розглядає їх у шести рівнях, використовуючи JavaScript та Express протягом усього тексту.
1. Аутентифікація: підтвердження особи користувача
Кожен захищений API повинен змусити клієнта підтвердити свою ідентичність перед виконанням будь-яких значущих дій, чи то через ім’я користувача та пароль, ключ API чи підписаний токен. Методи аутентифікації зазвичай поділяються на п’ять категорій: аутентифікація за ім’ям користувача та паролем, багатофакторна аутентифікація, аутентифікація на основі токенів, аутентифікація на основі сертифікатів та біометрія.
Поширеною причиною плутанини є те, що насправді замінює JWT. Традиційні серверські сесії зберігали стан на сервері та використовували куки браузера для передачі ідентифікатора сесії. JWT усуває необхідність пошуку на сервері з кожним запитом, але сам не перевіряє облікові дані: хтось все одно має перевірити пароль ще раз перед видачею токена. Саме тому добре підходить гібридний підхід. Користувач уходить підключається за допомогою електронної пошти та пароля, сервер при успішному підключенні видає JWT, а кожен наступний запит передає лише токен. Перевірка облікових даних та автентифікація з кожним запитом стають окремими завданнями, і пароль більше не передається з кожним викликом.
У Express бібліотека Passport підтримує обидва підходи за допомогою підключуваних стратегій. Налаштування вимагає трьох кроків.
Крок 1: зареєструвати локальну стратегію та стратегію JWT
Локальна стратегія виконується один раз під час входу та відповідає за пошук користувача за електронною поштою та порівняння введеного пароля з збереженим хешем. Стратегія JWT виконується з кожним захищеним запитом: вона отримує токен з заголовка Authorization: Bearer, перевіряє підпис за допомогою JWT_SECRET та визначає користувача, про якого йдеться у вмісті запиту. Експорт попередньо налаштованого проміжку authenticateJWT із параметром session: false чітко вказує на відсутність стану.
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");
// Local Strategy: Verify username and password during login.
passport.use(
new LocalStrategy(
{ usernameField: "email", passwordField: "password" },
async (email, password, done) => {
// Find the user and compare the hashed password.
// If valid, return the user.
}
)
);
// JWT Strategy: Verify the token on protected requests.
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (payload, done) => {
// Find the user referenced in the token.
}
)
);
// Middleware
const authenticateJWT = passport.authenticate("jwt", { session: false, });
module.exports = { passport, authenticateJWT, };
Функції повернення після перевірки залишаються тут у вигляді коментарів, і саме там знаходиться справжня робота з безпекою. Для порівняння паролів використовуйте повільний алгоритм хеширування з соллю, такий як bcrypt або Argon2, та повертайте однаковий загальний код помилки у разі невідомої електронної пошти чи неправильного пароля, щоб кінцева точка не могла використовуватися для виявлення наявних облікових записів.
Крок 2: автентифікація під час входу та підписування токена
Обробник входу викликає локальну стратегію через спеціальний калебек. Помилки передаються до механізму обробки помилок Express, відсутній користувач призводить до коду 401, а успішна автентифікація забезпечує токен, підписаний ідентифікатором та роллю користувача, з терміном дії, взятим з JWT_EXPIRES_IN, який за замовчуванням становить дві години.
const jwt = require("jsonwebtoken");
const passport = require("passport");
const login = (req, res, next) => {
passport.authenticate("local", { session: false }, (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json({ message: info.message, });
// Issue a signed JWT after successful authentication.
const token = jwt.sign(
{ id: user.id, role: user.role,},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || "2h",}
);
return res.status(200).json({ message: "Login successful.", token, user,});
})(req, res, next);
};
Два моменти заслуговують уваги. Короткий термін дії обмежує час, протягом якого вкрадений токен залишається корисним; якщо сесії потрібно підтримувати довше, слід поєднувати короткі токени доступу з процедурою оновлення, як це описано у нашій стратегії оновлення токенів для систем автентифікації Node.js. Крім того, у відповіді повертається об’єкт user у первісному вигляді. Якщо цей об’єкт є сирим рядком з бази даних, він може містити хеш пароля та внутрішні поля, що саме й є надмірним розкриттям даних, про яке йшлося раніше. Натомість слід повертати лише необхідні поля, такі як ID, електронна пошта та роль.
Крок 3: захист захищених маршрутів
Після експорту мідлверу захист маршруту полягає у розміщенні функції authenticateJWT перед контролером у визначенні маршруту. Запити без дійсного токена відхиляються ще до виконання будь-якої бізнес-логіки.
const { Router } = require("express");
const authRouter = Router();
// Get auth middleware and sample prorected controller
const authController = require("../controllers/auth.controller");
const { authenticateJWT } = require("../middleware/authentication");
// Use JWT as a guard to protect certain routes
authRouter.get("/me", authenticateJWT, authController.me);
authRouter.patch("/password", authenticateJWT, authController.updatePassword);
Якщо ви віддаєте перевагу легким рутерам, ту саму захисну функцію можна включити до власної ланцюга мідлверу контролера. У будь-якому разі нехай захист є стандартним для рутера, а публічні маршрути слід навмисно виключити з цього захисту, замість того щоб постійно пам’ятати додавати захисну функцію до кожного маршруту окремо.
2. Авторизація: визначення того, що може робити користувач
Аутентифікація відповідає на запитання „Хто ви?“; авторизація — на запитання „Що вам дозволено робити?“. Вона зазвичай виконується одразу після аутентифікації та перевіряє кожну ідентичність за правилами доступу перед тим, як схвалити чи відхилити запит. Без неї будь-який увійшовший користувач може читати конфіденційні дані або виконувати дії з привілеями, саме так і виникають вразливості типу BOLA.
Три моделі покривають більшість потреб:
- Контроль доступу на основі ролей (RBAC) надає права ролям, а ролі — користувачам. API для блогінгу може мати ролі адміністратора, редактора та переглядача. Він підходить для стабільних функцій та груп, тому часто використовується в корпоративних додатках.
- Контроль доступу на основі атрибутів (ABAC) оцінює атрибути користувача, ресурсу та середовища запиту (відділ, рівень конфіденційності ресурсу, час доби, мережа) з урахуванням правил. Він підходить для API, чиї рішення сильно залежать від контексту або часто змінюються.
- Контроль доступу на основі взаємозв’язків (ReBAC) надає доступ на основі взаємозв’язку між користувачем та певним ресурсом, таким як власність або приналежність до групи, що зазвичай перевіряється шляхом аналізу графа взаємозв’язків. Він ідеально підходить для спільних продуктів, таких як обмін документами чи соціальні платформи.
Самостійна налаштування системи авторизації — це чудовий спосіб зрозуміти її тонкощі, проте виробничі системи часто делегують процес видачі та перевірки токенів постачальнику ідентичностей. Коли API реєструється у такому постачальнику, як Microsoft Entra ID, та налаштовується для прийому токенів-носіїв, конфіденційні кінцеві точки перевіряють обсяги повноважень та ролі кожного токену перед виконанням запиту. Недійсний токен або відсутні права призводять до помилки 401 Unauthorized. У Express захищений маршрут виглядає так:
app.get(
"/api/orders",
passport.authenticate("oauth-bearer", { session: false }),
(req, res) => {
res.json({ message: "Protected resource." });
}
);
Пам’ятайте, що перевірений токен встановлює лише загальні права доступу. Перевірки на рівні об’єктів, наприклад „Чи належить це замовлення цьому користувачеві?“, все одно мають виконуватися у вашому обробнику або шарі даних, оскільки жоден постачальник ідентичностей не знає, хто є власником рядка 4812 у вашій базі даних.
Щодо самих протоколів, слід спиратися на галузеві стандарти, такі як OAuth 2.0, OpenID Connect та SAML. Ви можете самостійно реалізувати відповідні процеси або делегувати їх постачальникам сервісів ідентифікації, таким як Ping Identity, Okta, Microsoft Entra ID, AWS чи IBM Security Verify.
3. Шифрування: захист даних під час передачі та зберігання
Шифрування перетворює читабельні дані на зашифрований текст, який є безполезним без відповідного ключа. TLS захищає дані під час їх передачі; шифрування при зберіганні захищає їх у місцях зберігання, включаючи бази даних. Критично важливі системи зазвичай використовують обидва підходи, адже без шифрування дані, такі як фінансові облікові дані, можуть бути перехоплені або викрадені з компрометованих сховищ.
Основні підходи передбачають компроміс між швидкістю та управлінням ключами:
- Симетричне шифрування використовує один спільний ключ. Воно дуже швидке та ефективно обробляє великі обсяги даних, тому є ідеальним варіантом для шифрування даних у спокої та вмісту повідомлень, проте обидві сторони мають безпечно зберігати однаковий секретний ключ.
- Асиметричне шифрування використовує пару публічного та приватного ключів, тому немає потреби обмінюватися спільним секретом. Воно значно повільніше та практичне лише для невеликих обсягів даних.
- Гібридне шифрування поєднує обидва підходи: асиметрична криптографія захищає симетричний ключ, а симетричний ключ — основні дані. Таким чином досягається перевага обміну ключами з першого підходу та швидкість другого.
Для симетричного шифрування вбудований модуль crypto у Node підтримує AES-256-GCM. Наведений нижче обробник серіалізує тіло запиту, генерує новий вектор ініціалізації довжиною 12 байт, шифрує дані та повертає вектор ініціалізації, тег автентифікації GCM та зашифрований текст у вигляді шістнадцяткових рядків.
const crypto = require("crypto");
const algorithm = "aes-256-gcm";
const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
app.post("/api/orders", (req, res) => {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(JSON.stringify(req.body), "utf8"),
cipher.final(),
]);
const payload = {
iv: iv.toString("hex"),
tag: cipher.getAuthTag().toString("hex"),
data: encrypted.toString("hex"),
};
// Store or transmit the encrypted payload
res.json(payload);
});
Кілька факторів роблять цей підхід правильним. Ключ має мати розмір саме 32 байти (64 шістнадцяткові символи у ENCRYPTION_KEY) та походити з менеджера секретів, а не з вихідного коду. IV має бути унікальним для кожного зашифрування під однаковим ключем; повторне використання IV з алгоритмом GCM призводить до катастрофічних наслідків, тому він генерується для кожного запиту. Тег автентифікації дозволяє стороні, яка розшифровує дані, виявити зміни, тому його необхідно зберігати разом із зашифрованим текстом та перевіряти під час розшифрування. У справжньому сервісі цей пакет даних було б збережено або передано далі, а не надіслано назад викликаючому коду, як це робить демонстраційний варіант.
У тому ж модулі також доступне асиметричне шифрування. Дані, зашифровані публічним ключем, можна розшифрувати лише за допомогою відповідного приватного ключа:
const crypto = require("crypto");
const encrypted = crypto.publicEncrypt(
publicKey,
Buffer.from("Sensitive API data")
);
Оскільки RSA може шифрувати лише дані, розмір яких менший за розмір ключа, функція publicEncrypt підходить для коротких значень, таких як поле з секретом чи симетричний ключ, а не для цілих документів. Саме цю обмеженість усуває гібридний підхід: створюється тимчасовий ключ AES, за допомогою якого шифруються дані, а ключ AES шифрується публічним ключем RSA отримувача, після чого обидва елементи надсилаються. Отримувач використовує свій приватний ключ для розшифрування ключа AES, а потім — дань. Більшості API ніколи не знадобиться цей підхід у коді додатку, оскільки TLS вже здійснює подібний обмін, проте його варто зрозуміти для сценаріїв кінцево-кінцевого шифрування.
4. Перевірка та очищення вхідних даних
Як тільки ваш API приймає дані від клієнта, ви не можете передбачити, що саме надійде. Неправильно сформовані тіла запитів, фрагменти SQL-запитів та навантаження у вигляді скриптів виглядають як звичайні рядки, поки щось їх не інтерпретує. Для вирішення цієї проблеми існують дві взаємодоповнюючі техніки. Валідація відхиляє вхідні дані, які порушують ваші структурні та семантичні правила. Очищення перетворює прийняті дані на безпечну, нормалізовану форму перед тим, як вони потраплять до обробників.
Спочатку перевірте тип контенту
Найпростішою перевіркою є формат самого запиту. Цей невеликий мідлверт використовує req.is() для підтвердження значення Content-Type; у протилежному випадку він повертає код 415 Unsupported Media Type. Його можна встановити глобально, для кожного роутера чи окремого ендпоїнта.
const requireContentType = (type) => (req, res, next) => {
if (!req.is(type)) {
return res.status(415).json({ error: "Unsupported Media Type", });
}
next();
};
app.post("/api/users", requireContentType("application/json"),
(req, res) => {
res.json({ message: "User created." });
}
);
Перевірте форму та зміст тіла запиту
Після забезпечення правильного формату наступний етап перевіряє, чи запит сформований належним чином. За допомогою express-validator правила зберігаються у окремому модулі-валідатора. Цей модуль вимагає синтаксично коректної електронної пошти, виконує асинхронну користувацьку перевірку, яка відхиляє адреси, що вже є в базі даних, та забезпечує мінімальну довжину пароля у вісім символів.
const { body } = require("express-validator");
const { getUserEmail } = require("../db/queries");
const validateRegistration = [
body("email")
.isEmail()
.withMessage("Invalid email format")
.custom(async (value) => {
if (await getUserEmail(value)) {
throw new Error("Email is already in use");
}
return true;
}),
body("password")
.isLength({ min: 8 })
.withMessage("Password must be at least 8 characters long"),
];
module.exports = { validateRegistration }
Потім масив валідаторів додається до ланцюга мідлверів шляху. У функції-обробнику validationResult(req) збираються всі помилки, і шлях повертає код 400 із повним списком проблем замість того, щоб продовжити роботу.
const { validationResult } = require("express-validator");
const { validateRegistration } = require("../validators/userValidator");
app.post("/api/register", validateRegistration, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.json({ message: "Registration successful." });
});
Очищення після валідації
Оскільки Express обробляє мідлвери у порядку, ланцюг очищення може розташовуватися відразу після валідації. Тут ім’я обрізають та ескейпують від HTML, а електронну пошту нормалізують.
const sanitizeRegistration = [
body("firstName").trim().escape(),
body("email").normalizeEmail(),
];
app.post(
"/api/register",
validateRegistration,
sanitizeRegistration,
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
res.json({ message: "Registration successful." });
}
);
Тут порядок має важливе значення. Перевірка унікальності в валідаторі виконується перед функцією normalizeEmail(), тому два різні варіанти написання однієї адреси можуть пройти її та призвести до створення дублікатів облікових записів. Нормалізація перед пошуком або забезпечення унікальності нормалізованих даних на рівні бази даних усуває цю проблему. Також потрібно уважно ставитися до функції escape(): кодування HTML для вхідних даних захищає шаблони, які відображають ці дані, але змінює збережені дані; багато команд воліють зберігати сирі дані та кодувати їх під час виведення. Якщо ви розглядаєте бібліотеки для валідації, наша порівняльна стаття Zod та express-validator розглядає переваги та недоліки кожної з них.
Використовуйте параметризовані запити до бази даних
Ніколи не створюйте SQL-запити шляхом об’єднання даних, наданих користувачем. Клієнти баз даних, такі як pg, та ORM-інструменти, такі як Prisma, підтримують параметризовані запити, які надсилають текст запиту та значення окремо, щоб база даних завжди розглядала вхідні дані як звичайні дані, а не як виконуваний SQL-код.
За допомогою pg створіть пул з’єднань один раз та експортуйте його для ваших модулів даних:
const { Pool } = require("pg");
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
module.exports = pool;
Потім запити використовують проміжники з номерами ($1, $2) та значення надсилаються як окремий масив. Навіть якщо у email є лапка, за якою слідує DROP TABLE, це зберігається як літеральний рядок.
app.post("/api/users", async (req, res) => {
const { email, name } = req.body;
await pool.query(
"INSERT INTO users (email, name) VALUES ($1, $2)",
[email, name]
);
res.status(201).json({ message: "User created." });
});
Разом ці елементи створюють багатошарову систему обробки даних. Express-validator дозволяє ізолювати правила як повторно використовувані одиниці, які працюють як мідлвейр та повідомляють про кожну помилку, тоді як параметризація гарантує, що навіть дані, які пройшли валідацію, не зможуть змінити ваші запити.
5. Обмеження частоти та регулювання навантаження
Обмеження частоти визначає кількість запитів, які клієнт може надіслати протягом певного часового періоду. Воно ускладнює спроби атак типу brute-force та denial-of-service, а також запобігає ситуації, коли один інтенсивний користувач заважає роботі всіх інших.
Обмеження можна застосовувати у різних аспектах:
- Для кожного клієнта: запити підраховуються за кожним API-ключем або IP-адресою. Коли клієнт досягає ліміту, він чекає на його скасування або отримує більшу квоту, зазвичай у платному тарифі.
- За географічним розташуванням чи часом: обмеження відрізняються залежно від регіону чи часового проміжку; наприклад, дозволяється більше трафіку з регіонів, де працюють ваші клієнти, а обмеження посилюються у регіонах, звідки надходить підозрілий трафік.
Існує багато алгоритмів (фіксоване вікно, ковзне вікно, контейнер токенів), і вам не потрібно їх самостійно реалізовувати, щоб почати працювати. Мідлвейр express-rate-limit за замовчуванням підраховує кількість запитів за IP-адресою. У наведеному нижче прикладі встановлено загальний ліміт у 100 запитів протягом 15 хвилин для всього, що знаходиться під /api, а також значно суворіший ліміт у п’ять спроб протягом п’яти хвилин для процесу входу, з індивідуальним повідомленням для відхилених запитів.
const rateLimit = require("express-rate-limit");
// Apply to all API routes
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
});
// Apply stricter limits to authentication endpoints
const loginLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5,
message: "Too many login attempts. Please try again later.",
});
app.use("/api", apiLimiter);
app.post("/api/login", loginLimiter, (req, res) => {
res.json({ message: "Login successful." });
});
Публічні API зазвичай виділяють кожному користувачеві ключ, і обмеження за цим ключем є справедливішим, ніж обмеження за IP-адресою, оскільки багато користувачів можуть ділитися однією адресою через корпоративний проксі. Спеціальний keyGenerator читає заголовок X-API-Key та використовує його як ідентифікатор лічильника.
const rateLimit = require("express-rate-limit");
const apiKeyLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000,
keyGenerator: (req) => req.get("X-API-Key"),
});
app.use("/api", apiKeyLimiter);
У нинішньому вигляді кожен запит, який не містить цього заголовка, створює однаковий ключ undefined та використовує один і той самий бак. На практиці слід відхиляти запити без ключа на ранньому етапі обробки або переходити на використання IP-адреси.
Обмеження кількості запитів до дорогих ендпоїнтів
Регулювання швидкості визначає, наскільки швидко приймаються запити, щоб раптові сплески не перевантажували сервіс. У наступній конфігурації використовується той самий проміжний сервіс із дуже короткими інтервалами: максимум десять запитів на секунду через API, а також лише один запит на пошук кожні дві секунди від кожного клієнта, оскільки пошук є найбільш ресурсоємним ендпоїнтом.
const rateLimit = require("express-rate-limit");
// Throttle all API requests
const apiThrottle = rateLimit({
windowMs: 1000, // 1 second
max: 10, // Allow up to 10 requests per second
});
// Apply a stricter throttle to resource-intensive endpoints
const searchThrottle = rateLimit({
windowMs: 2000, // 2 seconds
max: 1, // Allow 1 request every 2 seconds
message: "Please wait before sending another search request.",
});
app.use("/api", apiThrottle);
app.get("/api/search", searchThrottle, (req, res) => {
res.json({ results: [] });
});
Якщо говорити точніше, це все ще обмеження частоти запитів з короткими інтервалами: надмірні запити відхиляються з кодом 429, а не з затримкою. Якщо ви хочете справжнє обмеження швидкості, яке уповільнює клієнтів перед відмовою, слід використати додатковий пакет, такий як express-slow-down, який додає поступові затримки. Також зверніть увагу, що стандартний внутрішній механізм зберігання підраховує запити за кожним процесом, тому за лоад-балансером з кількома інстанціями потрібен спільний механізм зберігання, наприклад Redis, щоб обмеження залишалися актуальними. У новіших версіях express-rate-limit опція max також називається limit; перевірте документацію до версії, яку ви інсталюєте. Якщо вам потрібна легка альтернатива на TypeScript, ознайомтесь з нашою статтею про мінімальний обмежувач частоти запитів для Express.
6. Логування, моніторинг та виявлення інцидентів
Ви не можете відповісти на атаку, яку ніколи не бачите. Журналування фіксує запити та відповіді разом із їхньою метаданими, контекстом, часом виконання та кодами помилок, щоб ви могли усувати проблеми, проводити аудит та розуміти фактичне використання. Моніторинг спостерігає за активністю в реальному часі, відстежує такі показники, як затримка, частота помилок та пропускна здатність, а також виявляє аномалії, які можуть свідчити про зловживання, недосягнення цілей рівня послуги чи експлуатацію вразливостей.
Основні підходи та їхні переваги та недоліки:
- Журналування запитів за допомогою проміжного програмного забезпечення, такого як Morgan, дешево фіксує кожен надходячий HTTP-запит, але не дає інформації про стан системи.
Будь-який з цих інструментів підключається до Express. Далі наведені чотири поширених елементи.
Логування запитів за допомогою Morgan
Реєстрація Morgan у форматі combined записує рядок у стилі Apache для кожного запиту, включаючи метод, шлях, статус, розмір відповіді та user agent.
const express = require("express");
const morgan = require("morgan");
const app = express();
// Log every incoming request
app.use(morgan("combined"));
app.get("/api/users", (req, res) => {
res.json({ message: "Users retrieved successfully." });
});
Структуроване логування додатку за допомогою Winston
Winston записує події у вигляді структурованих об’єктів. Логування ідентифікаторів користувача та замовлень під час їх створення створює аудитну історію, яку можна згодом пошукати.
const winston = require("winston");
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
],
});
app.post("/api/orders", (req, res) => {
logger.info("Order created", {
userId: req.user.id,
orderId: req.body.id,
});
res.status(201).json({ message: "Order created." });
});
Будьте обережні з тим, що потрапляє до логів. Ідентифікатори користувачів — це гаразд; паролі, токени, повні номери карток та весь контент запиту — ні, а логи є поширеним джерелом витоку конфіденційних даних.
Централізоване логування помилок
Проміжок обробки помилок Express, який впізнаваний за своїми чотирма аргументами, ловить помилки з будь-якого маршруту. Він записує повідомлення разом із шляхом та методом та повертає загальний код 500, щоб стек-трейси та внутрішні деталі ніколи не потрапляли до клієнта.
app.use((err, req, res, next) => {
/* Logger is built as an independent module or class */
logger.error(err.message, {
path: req.originalUrl,
method: req.method,
});
res.status(500).json({
error: "Internal Server Error",
});
});
Розкриття метрик для Prometheus
Бібліотека prom-client збирає стандартні метрики процесу Node.js та розкриває їх на кінцевій точці /metrics для збору даних Prometheus.
const client = require("prom-client");
client.collectDefaultMetrics();
app.get("/metrics", async (req, res) => {
res.set("Content-Type", client.register.contentType);
res.end(await client.register.metrics());
});
Ця кінцева точка розкриває внутрішні деталі вашого сервісу, тому обмежте доступ до неї мережею моніторингу або розмістіть її за автентифікацією, замість того щоб залишити її загальнодоступною.
Фреймворки з чітко визначеною архітектурою допомагають у цьому. NestJS постачається з вбудованим логгером та структурою, яка полегшує підключення функцій логування та метрик до кожного ендпоїнта, тоді як у фреймворках без чітко визначеної архітектури, таких як Express, доводиться особисто додавати функції логування, зазвичай у вигляді мідлверу, який працює перед надсиланням відповіді. Виявлення інцидентів ґрунтується на цих логах: сповіщення про підозрілі закономірності, такі як стрибки кількості відповідей типу 401 або відмови через обмеження частоти запитів, надсилаються у канал сповіщень, який використовує ваша команда.
Безпека як безперервний процес
Жодна окрема стаття не охоплює все, але перед кожним випуском ви можете переконатися, що ваш API відповідає цим базовим вимогам:
- Кожен ендпоїнт обслуговується лише через HTTPS.
- Встановлений механізм OAuth або еквівалентний процес отримання токенів.
- Видалені JWT мають термін дії.
- Обмеження захищають усі маршрути, причому більш суворі — для процедури входу.
Для прикладів тут використовувався Express, але ці ж принципи застосовні до інших фреймворків бекенду, більшість з яких або безпосередньо інтегрують ці інструменти, або пропонують їхні власні еквіваленти. Наприклад, NestJS здійснює перевірку запитів через об’єкти передачі даних. У документації вашого фреймворку буде наведена стандартна версія кожного рівня.
Ті самі принципи є основою для розгортання за технологією cloud-native на платформах таких як Azure, Google Cloud та AWS, які додають власні шлюзи, сервіси ідентифікації та механізми керованого обмеження швидкості. Практики, специфічні для хмарних середовищ, потребують окремого розгляду, але вищезазначені шари вже значною мірою допомагають API пройти перевірку на безпеку.
Ключові висновки
- Розділяйте перевірку облікових даних та автентифікацію за кожним запитом: перевіряйте пароль один раз, а потім використовуйте токени з коротким терміном дії та підписом.
- Автентифікація — це не авторизація. Перевіряйте діапазони та ролі, а також власність кожного об’єкта, до якого стосується запит.
- Для даних у спокої використовуйте AES-GCM із унікальним IV для кожної операції; асиметричне шифрування залиште для невеликих значень та обміну ключами.
Пов’язана література
- Коли JWT Auth стає залежним від стану: аргументи на користь сеансів з боку сервера в Node — Дізнайтеся, як списки заборон та механізми оновлення роблять автентифікацію JWT вразливою, як сеанси Express з підтримкою Postgres її спрощують, та де JWT все ще мають сенс.
- Створення автентифікації JWT готової до використання у продакшені в API на Node.js — як хешувати паролі, випускати тимчасові JWT, додавати токени оновлення та механізми скасування, розділяти авторизацію та автентифікацію, протидіяти атакам типу brute force та перевіряти правильність відмов у автентифікації.
- Біометрична автентифікація рівня підвищеної безпеки на iOS та Android з ключами, прив’язаними до обладнання — дізнайтеся, чому сам Face ID не є достатнім доказом для вашого сервера, та як використовувати ключі Secure Enclave та Android Keystore для підпису одноразових викликів від сервера.
- Від завантаження до URL: безпечне зберігання та обслуговування файлів користувачів у Express — Дізнайтеся, де слід зберігати завантажені файли в додатках Express, як express.static мапує папки на URL-адреси та які заходи безпеки запобігають перетворенню завантажених користувачами файлів на вразливості.
- Контроль доступу за ролями в Express з використанням JWT та двох проміжних модулів — Дізнайтеся, як забезпечити контроль доступу за ролями в API Express шляхом поєднання проміжного модуля автентифікації JWT з повторно використовуваним захисним механізмом authorize(), а також коли слід повертати коди 401 та 403.