Суть Читай реальные сниппеты auth/веб-приложения поперёк трека — валидация JWT, объектная авторизация, конфиг куки/CSRF, хранение паролей — и выбирай фикс с наибольшим рычагом, который security-сеньор делает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги безопасности читаются в коде и конфигах, а не в лозунгах. Каждый сниппет ниже — небольшой кусок реального стека безопасного приложения. Прочитай, найди шов и выбери фикс, который сеньор делает раньше всего — тот же цикл, что и на ревью или в инциденте.
Цель
Отработай цикл ревью поперёк всего трека: заметь сломанную проверку токена, отсутствующую проверку владения, куку, которая снова открывает CSRF, и секрет, превращающий конфиг в брешь — затем выбери фикс с наибольшим рычагом.
Эта мидлварь аутентифицирует каждый запрос. В чём критический изъян и фикс?
Heads-up Парсинг header в порядке. Фатальный баг — декод без верификации подписи; кука не починит поддельные неподписанные claims.
Heads-up Optional chaining уже защищает от отсутствующего header. Помимо устойчивости, дефект безопасности — доверие декодированным claims без верификации подписи вообще.
Heads-up TLS защищает транзит; он ничего не делает с поддельным payload. Атакующий пишет свой токен — это останавливает только верификация подписи с закреплённым alg.
Токен валидирован и scope проверен. Пользователь с валидным токеном всё равно читает statements чужого аккаунта. Почему и в чём фикс?
Heads-up Запрос использует параметр ($1), инъекции нет. Изъян — отсутствующая объектная авторизация: валидный токен и scope не доказывают владение этим id.
Heads-up Read scope корректен для GET. Scope — грубое разрешение, а не владение конкретным объектом. Баг — отсутствие проверки owns(subject, object) на конкретном аккаунте.
Heads-up Scope авторизует класс действия (можно читать statements), а не конкретную запись. Без проверки владения это IDOR, A01 у OWASP.
Сниппет 3 — сессионная кука
res.cookie("session", token, { httpOnly: true, secure: true, // sameSite не задан});// в другом месте, «удобный» маршрутapp.get("/account/delete", deleteAccountHandler); // изменение состояния на GET
Викторина
Completed
HttpOnly и Secure заданы, поэтому XSS не прочитает куку. Какая экспозиция CSRF остаётся и как её закрыть?
Heads-up HttpOnly останавливает чтение из JS, а не автоприкрепление куки браузером cross-site. Изменяющий состояние GET — учебная цель CSRF; HttpOnly к нему нерелевантен.
Heads-up Шифрование значения куки не мешает браузеру слать её на поддельный cross-site запрос. Фикс — SameSite плюс CSRF-токен и перенос действия с GET.
Heads-up Сброс Secure — регрессия, разрешающая куку по plaintext. Это ничего не даёт против CSRF и ослабляет транспортную безопасность.
В этих нескольких строках сидят два отказа поперёк трека. Назови оба и фикс для каждого.
Heads-up Приватное репо всё равно утекает через клоны, CI-логи и случайную публикацию, а история хранит значение после удаления. Закоммиченный секрет уже скомпрометирован — ротируй его.
Heads-up SHA-256 криптографичен, но быстр (~22 миллиарда/с на GPU) и здесь несолён — ровно неправильно для паролей. Нужен медленный, солёный, memory-hard KDF.
Heads-up Соль побеждает rainbow-таблицы, но оставляет GPU-скорость SHA-256, поэтому распространённые пароли всё равно быстро ломаются офлайн. Фикс — намеренно медленный KDF, а не просто соль.
Итог
Каждый сниппет был швом между двумя юнитами трека. jwt.decode ничего не аутентифицирует — верифицируй подпись и закрепляй алгоритм. Валидный токен и scope — это не владение; добавь объектную проверку owns(subject, object), чтобы убить IDOR. HttpOnly останавливает кражу через XSS, но не CSRF — задай SameSite, добавь токен и держи изменения состояния подальше от GET. А закоммиченный секрет уже взломан (ротируй, не удаляй), тогда как быстрый несолёный хеш отдаёт столбец паролей (используй Argon2id с солью на пользователя). Читай код, находи шов между правильными с виду слоями и чини структурный пробел.