Безопасность
Эшелонированная защита: брешь живёт в шве
Флоу входа — образцовый. Пароли хешируются Argon2id, сессия — короткоживущий JWT, TLS принуждается через HSTS, API-ключ лежит в вольте. А потом пользователь меняет id в GET /accounts/{id}/statements и читает чужие банковские выписки. Ни один контроль не сломан. Аутентификация проверила, кто вызывает, — безупречно — а эндпоинт выписок просто никогда не спросил, имеет ли этот вызывающий право читать эту запись. Каждый слой прошёл свой собственный тест. Брешь жила в зазоре между ними.
Один запрос, шесть слоёв
Проследи одно чувствительное действие — залогиненный пользователь переводит деньги — и ты пройдёшь через все контроли, что покрыл трек безопасности. Каждый — отдельный слой со своей задачей, и сеньор рассуждает о них как о стеке, а не как о чек-листе:
- Транспорт — TLS плюс HSTS, чтобы запрос нельзя было прочитать или даунгрейднуть до открытого текста на проводе.
- Аутентификация — доказать, кто вызывает. Короткоживущий access-токен (минуты, не дни), пароли хранятся как хеши Argon2id или bcrypt, никогда сам пароль.
- Авторизация — решить, имеет ли этот вызывающий право делать это с этим объектом. Запрет-по-умолчанию, наименьшие привилегии. Это риск №1 у OWASP не просто так.
- Обработка ввода — относись к каждому полю как к враждебному: валидируй, параметризуй запросы, кодируй вывод.
- Секреты — ключи и креды БД живут в вольте или env, никогда в репозитории, никогда в JWT.
- Цепочка поставок — зависимости, исполняющие всё вышеперечисленное, сами проверены и запинены.
Тезис всего трека приземляется здесь: эшелонированная защита предполагает, что каждый контроль рано или поздно откажет. Ты складываешь их так, чтобы, когда первый барьер падёт, второй замедлил атакующего, а третий поднял тревогу. Ошибка джунов — считать один сильный контроль ответом.
Брешь живёт в шве
Опасные провалы — это не отсутствующий слой, такие ловят на ревью. Это два корректных слоя, которые не композируются. Хук — каноничный пример: аутентификация ответила «да, это валидный залогиненный пользователь», а эндпоинт доверил этому ответу значение «…значит, пусть читает запись», чего аутентификация никогда не утверждала. AuthN — это не AuthZ. Знание, кто человек, не говорит ничего о том, к чему он может прикасаться.
OWASP ставит Broken Access Control на A01 — самый частый серьёзный изъян — именно потому, что он живёт в этих швах. Фикс структурный, а не заплатка на один роут: принуждай авторизацию в доверенном серверном коде, запрещай по умолчанию для каждого непубличного ресурса и проверяй владение записью на объекте, который называет запрос, а не только на роуте. Атакующий, переключающий {id} в GET /accounts/{id}/statements, делает IDOR; останавливает это только сервер, спрашивающий «владеет ли этот субъект этим объектом?» на каждом запросе.
| Слой | По отдельности выглядит верно | Шов, который всё равно его ломает |
|---|---|---|
| Аутентификация (JWT) | Подписан, короткоживущий, проверен | Эндпоинт не проверяет owns(subject, object) → IDOR |
| Хеширование пароля | Argon2id, с солью, ~250мс | У эндпоинта сброса нет рейт-лимита → захват аккаунта |
| Хранение токена | Сильный, валидный JWT | Лежит в localStorage → один XSS читает всё |
| Защита от CSRF | Кука SameSite=Lax | Меняющий состояние роут на GET обходит её полностью |
Шов JWT-в-localStorage
Решение о хранении токена — чистая проблема композиции, поэтому заслуживает близкого взгляда. Безупречно выпущенный JWT — подписанный, короткий срок, верные claims — побеждается тем, куда ты его кладёшь. Положи его в localStorage, и любой успешный XSS выполняет localStorage.getItem('token') и эксфильтрует его на сервер атакующего; собственная сила токена нерелевантна, как только JavaScript может его прочитать. Митигация — HttpOnly-кука, которую JavaScript не может тронуть, поэтому XSS её не украдёт — но куки шлются автоматически кросс-сайтово, что вновь открывает CSRF. Это ты закрываешь через SameSite плюс CSRF-токен. Заметь форму: каждый фикс решает один слой и обнажает шов со следующим. Нет единой настройки, которая побеждает; ты композируешь HttpOnly + Secure + SameSite + короткий TTL, и всё равно обязан убить XSS у источника кодированием вывода и CSP, потому что атакующий, способный исполнить скрипт в твоём origin, уже прошёл мимо вопроса хранения.
Почему это работает
«Мы используем JWT, значит, мы stateless и в безопасности» смешивает две несвязанные вещи. JWT доказывает аутентификацию и может нести claims; он ничего не говорит об авторизации на конкретном объекте и ничего не делает для защиты собственного хранения. Statelessness — это выбор архитектуры. Устойчивость к IDOR и XSS — отдельные контроли, которые ты всё равно обязан построить: формат токена не даёт ни того, ни другого.
Наименьшие привилегии везде, не только у двери
Эшелонированная защита — ещё и про радиус взрыва. Когда слой всё же откажет — а модель предполагает, что один откажет, — наименьшие привилегии решают, как далеко расползётся ущерб. Кред из вольта, что использует API, должен давать только те строки и операции, что нужны эндпоинту, чтобы скомпрометированный сервис не осушил всю базу. Access-токен должен быть короткоживущим, чтобы украденный истёк за минуты, а не недели. Зависимость, которую ты подтянул, должна работать под тем же надзором, потому что вредоносный транзитивный пакет исполняется внутри твоей границы доверия. NIST SP 800-63B и руководство OWASP по паролям задают пол для слоя аутентификации — Argon2id, настроенный примерно на 250–500мс на хеш, или bcrypt с cost factor 12+, с уникальной солью на пользователя, — но сильный хеш за эндпоинтом сброса без рейт-лимита всё равно падает. Число на одном слое никогда не оправдывает зазор в следующем.
Залогиненный пользователь вызывает `GET /accounts/{id}/statements` с валидным подписанным JWT, но с id, который не его. Что на самом деле останавливает брешь?
Сильный подписанный JWT хранится в localStorage. Каким реалистичным способом его крадут?
Что фундаментально предполагает «эшелонированная защита» о твоих контролях?
Расставь слои, через которые проходит один чувствительный запрос, от внешнего к внутреннему:
- 1 Транспорт: TLS + HSTS, чтобы запрос нельзя было прочитать или даунгрейднуть
- 2 Аутентификация: доказать, кто вызывает (короткоживущий токен, хешированный пароль)
- 3 Авторизация: запрет-по-умолчанию, проверить, что этот субъект владеет этим объектом
- 4 Обработка ввода: валидировать, параметризовать, кодировать каждое поле
- 5 Секреты + зависимости: ключи в вольте, пакеты проверены и запинены
- 01Коллега говорит, что флоу входа безопасен, потому что пароли хешируются Argon2id и JWT сильный. Объясни, почему этого недостаточно и где прячется настоящая брешь.
- 02Пройди по тому, почему безупречно валидный JWT всё равно крадут, и как выглядит композиция контролей на практике.
Один чувствительный запрос проходит через слои — транспорт, аутентификация, авторизация, обработка ввода, секреты, цепочка поставок — и трек безопасности покрыл каждый. Урок их сборки воедино в том, что ни один контроль не достаточен, а опасные провалы — это не отсутствующие слои, а два корректных слоя, которые не композируются. Аутентификация доказывает, кто вызывает, и ничего не говорит о том, может ли он трогать конкретный объект, поэтому Broken Access Control — риск №1 у OWASP: брешь живёт в шве, эндпоинт доверяет валидному токену значение владения. Сильный хеш Argon2id падает за эндпоинтом сброса без рейт-лимита; сильный JWT эксфильтруется из localStorage одним XSS. Эшелонированная защита предполагает, что каждый контроль рано или поздно откажет, поэтому ты складываешь независимые слои, запрещаешь по умолчанию, применяешь наименьшие привилегии к кредам и срокам жизни токенов и принуждаешь авторизацию на объекте — не только на личности — на каждом запросе. Моделируй угрозы всему флоу, а не каждому контролю по отдельности, потому что зазор между двумя корректными контролями — ровно там, где живут атакующие.