Суть Читай реальные сниппеты верификации JWT и поддельный токен, находи уязвимость и выбирай фикс наибольшего рычага, который security-минд senior делает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги JWT прячутся в вызове verify и проверках claim после него. Читай каждый сниппет так, как делал бы это в security-ревью, найди, что эксплуатирует атакующий, и выбери фикс, прежде чем дописать ещё строку.
Цель
Отработай цикл ревью, который запускаешь на каждом auth-пути: читай вызов verify, спроси, что контролирует атакующий, и тянись к фиксу «пинь-и-валидируй», закрывающему брешь доверия.
Сниппет 1 — поддельный токен alg:none
header: { "alg": "none", "typ": "JWT" }payload: { "sub": "victim", "role": "admin" }signature: (empty)// код сервераconst claims = jwt.decode(token); // только decode, без verifyif (claims.role === "admin") grantAdmin();
Викторина
Completed
Что этот код даёт атакующему и какой фикс?
Heads-up decode парсит base64url и возвращает claims независимо от signature; он её никогда не валидирует. Токен с пустой signature декодируется нормально, и проверка role проходит.
Heads-up Опасность здесь даже не в обработке alg:none — код вызывает decode, который полностью пропускает верификацию. Даже корректно подписывающий верификатор обойдён, потому что verify не вызывается.
Heads-up Сниппет выдаёт admin чисто по непроверенному claim role. В этом пути кода нет второго фактора; поддельный claim используется напрямую.
Атакующий подделывает токен с alg:HS256, подписанный содержимым rsa-public.pem как HMAC-секретом. Почему verifyToken его принимает?
Heads-up Без явного allowlist библиотека выбирает алгоритм из заголовка, а не из расширения файла ключа. Заголовок говорит HS256, поэтому запускается HMAC-путь с публичным ключом как секретом.
Heads-up Длина ключа нерелевантна путанице; публичный ключ используется дословно как HMAC-секрет. Баг — вообще позволять заголовку выбрать HS256.
Heads-up Файл читается корректно. Атака воспроизводит точное HMAC-вычисление сервера, используя ту же строку публичного ключа как секрет — без всякой порчи.
Сниппет 3 — signature проверена, claim нет
const claims = jwt.verify(token, key, { algorithms: ["RS256"] });// signature OK — теперь доверяем всемуreq.user = claims.sub;req.roles = claims.roles;// нет проверок exp / iss / aud сверх дефолта библиотеки
Викторина
Completed
Алгоритм запинен и signature проверена. В микросервисной сети, делящей один ключ подписи, что всё ещё не так?
Heads-up Валидная signature лишь доказывает аутентичность выпуска. Без aud любой сервис, делящий ключ, принимает токены, выпущенные для любого другого сервиса в сети.
Heads-up Порядок чтения полей нерелевантен. Дефект — принятие токена, чей audience — совсем другой сервис, потому что aud никогда не валидируется.
Heads-up Выбор алгоритма не связан с отсутствующей проверкой audience. Переход на HS256 наоборот расширит риск путаницы, а не починит привязку audience.
Как атакующий полностью контролирует верификацию здесь и какой фикс?
Heads-up HTTPS аутентифицирует собственный сервер атакующего, а не то, что ключ принадлежит тебе. Заголовок выбрал URL, поэтому безопасность транспорта не остановит ключ, который контролирует атакующий.
Heads-up Кэширование переданного атакующим ключа лишь закрепляет неверный ключ. Уязвимость — вообще брать ключ с названного заголовком URL, а не частота fetch.
Heads-up Собственный JWKS атакующего по построению содержит совпадающий kid. Сверка с набором, захостенным атакующим, ничего не доказывает; набор ключей должен браться из доверенного серверного источника.
Итог
Каждый дефект JWT читается в вызове verify и проверках вокруг него: decode-only код доверяет неподписанным claim; отсутствующий allowlist algorithms позволяет заголовку выбрать HS256 и превращает публичный ключ в HMAC-секрет; проверенная signature без проверки aud/iss — это ждущий своего часа confused-deputy в сети с общим ключом; а резолв kid/jku из контролируемых атакующим заголовков отдаёт выбор ключа атакующему. Фикс всегда одной формы — пинь алгоритм, привязывай и allowlist-и ключ, затем валидируй зарегистрированные claim против того, что ожидает этот сервис.