Большинство взломов бэкенда происходит через несколько классических дыр. На собесе ждут, что ты назовёшь их, покажешь уязвимый код и исправление.
Инъекции
SQL-инъекция появляется, когда пользовательские данные склеиваются со строкой запроса:
// уязвимо: id = "1 OR 1=1" вернёт всех
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// правильно: параметризованный запрос
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
При параметризации значение уходит в базу отдельно от текста запроса и не может стать частью SQL. ORM (Prisma, TypeORM) параметризуют сами, но «сырые» методы вроде $queryRawUnsafe снова открывают дыру.
NoSQL-инъекция: в MongoDB объект { "password": { "$ne": null } } из JSON-тела совпадёт с любым паролем. Лечится валидацией типов: пароль должен быть строкой.
Command injection: строка с пользовательским вводом в exec. Аргументы передают массивом в execFile.
Валидация входа
Всё, что пришло извне (тело, query, заголовки, файлы), валидируй схемой на границе: zod, class-validator, JSON Schema. Это заодно защищает от mass assignment: если передать req.body целиком в user.update, злоумышленник допишет "role": "admin". Бери только разрешённые поля.
Пароли
Пароли не шифруют и не хешируют быстрыми функциями (MD5, SHA-256): видеокарта перебирает миллиарды таких хешей в секунду. Нужна медленная функция с солью: argon2id, scrypt (есть в node:crypto) или bcrypt. Соль уникальна для каждого пользователя и хранится рядом с хешем, она ломает радужные таблицы.
Секреты и токены сравнивай crypto.timingSafeEqual, а не ===: обычное сравнение выходит на первом несовпавшем байте, и по времени ответа можно подбирать значение.
Ограничение частоты
Rate limiting защищает вход, восстановление пароля, отправку SMS и дорогие эндпоинты от перебора и злоупотреблений. При нескольких экземплярах счётчики хранят в Redis, иначе лимит умножается на число экземпляров. Ответ при превышении: 429 Too Many Requests.
Пути и файлы
Path traversal: path.join('/srv/uploads', '../../etc/passwd') даёт /etc/passwd. Проверяй, что итоговый path.resolve(...) начинается с разрешённой папки, а лучше храни файлы под сгенерированными именами.
Prototype pollution
Рекурсивный merge пользовательского JSON может записать свойства в Object.prototype через ключ __proto__. После этого у всех объектов появится, например, isAdmin: true. Защита: валидация схемой, Object.create(null) или Map для словарей, отказ от самописного глубокого слияния, пропуск ключей __proto__, constructor, prototype.
Другое
- ReDoS: регулярка с вложенными квантификаторами вроде
/(a+)+$/на специально подобранной строке работает экспоненциально и блокирует event loop. - SSRF: сервер скачивает URL, присланный пользователем, и ходит во внутреннюю сеть или к метаданным облака. Разрешай только список доменов и проверяй IP.
- Секреты: в переменных окружения или менеджере секретов, не в git. Утёкший ключ меняют (ротация), а не только удаляют из истории.
- Заголовки:
helmetвыставляет HSTS,X-Content-Type-Options, CSP и другие. - Зависимости: lock-файл,
npm auditв CI, обновления. Каждый пакет выполняет код с правами твоего процесса. - Ошибки: клиенту не отдают стек и текст внутренних исключений.
- Лимит тела запроса: без него JSON на гигабайт съест память.
Что спросят на собесе
- Как защититься от SQL-инъекции и почему экранирования вручную мало.
- Как хранить пароли.
- Что такое rate limiting и как сделать его на нескольких экземплярах.
- Что такое prototype pollution или path traversal.
- Где хранить секреты.