open atlas
↑ К треку
Безопасность SEC · 08 · 01

Эшелонированная защита: брешь живёт в шве

Ни один контроль безопасности не достаточен сам по себе. Каждый слой — TLS, аутентификация, авторизация, ввод, секреты, зависимости — может быть корректным по отдельности, а система всё равно падает: брешь живёт в шве, где встречаются два корректных контроля.

SEC Junior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Флоу входа — образцовый. Пароли хешируются Argon2id, сессия — короткоживущий JWT, TLS принуждается через HSTS (HTTP Strict Transport Security — заголовок, запрещающий браузеру переходить на HTTP), API-ключ лежит в вольте. А потом пользователь меняет id в GET /accounts/{id}/statements и читает чужие банковские выписки. Ни один контроль не сломан. Аутентификация проверила, кто вызывает, — безупречно — а эндпоинт выписок просто никогда не спросил, имеет ли этот вызывающий право читать эту запись. Каждый слой прошёл свой собственный тест. Брешь жила в зазоре между ними.

Один запрос, шесть слоёв

Проследи одно чувствительное действие — залогиненный пользователь переводит деньги — и ты пройдёшь через все контроли, что покрыл трек безопасности. Каждый — отдельный слой со своей задачей, и сеньор рассуждает о них как о стеке, а не как о чек-листе:

  1. Транспорт — TLS плюс HSTS, чтобы запрос нельзя было прочитать или даунгрейднуть до открытого текста на проводе.
  2. Аутентификация — доказать, кто вызывает. Короткоживущий access-токен (минуты, не дни), пароли хранятся как хеши Argon2id или bcrypt, никогда сам пароль.
  3. Авторизация — решить, имеет ли этот вызывающий право делать это с этим объектом. Запрет-по-умолчанию, наименьшие привилегии. Это риск №1 у OWASP не просто так.
  4. Обработка ввода — относись к каждому полю как к враждебному: валидируй, параметризуй запросы, кодируй вывод.
  5. Секреты — ключи и креды БД живут в вольте или env, никогда в репозитории, никогда в JWT.
  6. Цепочка поставок — зависимости, исполняющие всё вышеперечисленное, сами проверены и запинены.

Тезис всего трека приземляется здесь: эшелонированная защита предполагает, что каждый контроль рано или поздно откажет. Ты складываешь их так, чтобы, когда первый барьер падёт, второй замедлил атакующего, а третий поднял тревогу. Ошибка джунов — считать один сильный контроль ответом. Вместе эти шесть слоёв означают, что бреши приходится пробивать каждый по очереди — и опасный зазор никогда не внутри одного слоя, а в шве, где два слоя встречаются, не скомпоновавшись.

Брешь живёт в шве

Опасные провалы — это не отсутствующий слой, такие ловят на ревью. Это два корректных слоя, которые не композируются. Хук — каноничный пример: аутентификация ответила «да, это валидный залогиненный пользователь», а эндпоинт доверил этому ответу значение «…значит, пусть читает запись», чего аутентификация никогда не утверждала. AuthN — это не AuthZ. Знание, кто человек, не говорит ничего о том, к чему он может прикасаться.

OWASP ставит Broken Access Control на A01 — самый частый серьёзный изъян — именно потому, что он живёт в этих швах. Фикс структурный, а не заплатка на один роут: принуждай авторизацию в доверенном серверном коде, запрещай по умолчанию для каждого непубличного ресурса и проверяй владение записью на объекте, который называет запрос, а не только на роуте. Атакующий, переключающий {id} в GET /accounts/{id}/statements, делает IDOR (Insecure Direct Object Reference — прямое обращение к чужому объекту по угаданному идентификатору); останавливает это только сервер, спрашивающий «владеет ли этот субъект этим объектом?» на каждом запросе.

СлойПо отдельности выглядит верноШов, который всё равно его ломает
Аутентификация (JWT)Подписан, короткоживущий, проверенЭндпоинт не проверяет owns(subject, object) → IDOR
Хеширование пароляArgon2id, с солью, ~250мсУ эндпоинта сброса нет рейт-лимита → захват аккаунта
Хранение токенаСильный, валидный JWTЛежит в localStorage → один XSS (Cross-Site Scripting — внедрение постороннего скрипта на страницу) читает всё
Защита от CSRFКука SameSite=LaxМеняющий состояние роут на GET обходит её полностью

Шов JWT-в-localStorage

Когда слышишь «мы используем JWT», спроси себя: где живут эти токены и что до них может дотянуться? Решение о хранении токена — чистая проблема композиции, поэтому заслуживает близкого взгляда. Безупречно выпущенный 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. 1 Транспорт: TLS + HSTS, чтобы запрос нельзя было прочитать или даунгрейднуть
  2. 2 Аутентификация: доказать, кто вызывает (короткоживущий токен, хешированный пароль)
  3. 3 Авторизация: запрет-по-умолчанию, проверить, что этот субъект владеет этим объектом
  4. 4 Обработка ввода: валидировать, параметризовать, кодировать каждое поле
  5. 5 Секреты + зависимости: ключи в вольте, пакеты проверены и запинены
Вспомните перед уходом
  1. 01
    Коллега говорит, что флоу входа безопасен, потому что пароли хешируются Argon2id и JWT сильный. Объясни, почему этого недостаточно и где прячется настоящая брешь.
  2. 02
    Пройди по тому, почему безупречно валидный JWT всё равно крадут, и как выглядит композиция контролей на практике.
Итог

Один чувствительный запрос проходит через слои — транспорт, аутентификация, авторизация, обработка ввода, секреты, цепочка поставок — и трек безопасности покрыл каждый. Урок их сборки воедино в том, что ни один контроль не достаточен, а опасные провалы — это не отсутствующие слои, а два корректных слоя, которые не композируются. Аутентификация доказывает, кто вызывает, и ничего не говорит о том, может ли он трогать конкретный объект, поэтому Broken Access Control — риск №1 у OWASP: брешь живёт в шве, эндпоинт доверяет валидному токену значение владения. Сильный хеш Argon2id падает за эндпоинтом сброса без рейт-лимита; сильный JWT эксфильтруется из localStorage одним XSS. Эшелонированная защита предполагает, что каждый контроль рано или поздно откажет, поэтому ты складываешь независимые слои, запрещаешь по умолчанию, применяешь наименьшие привилегии к кредам и срокам жизни токенов и принуждаешь авторизацию на объекте — не только на личности — на каждом запросе. Теперь, когда видишь «безопасный» флоу входа, первый вопрос — не «насколько силён JWT?», а «какой шов этот флоу не закрывает?». Моделируй угрозы всему потоку, а не каждому контролю по отдельности, и найди зазор раньше, чем его найдёт атакующий.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.