JWT: структура и уязвимости
1) Что такое JWT и где он используется
JWT — компактный самодостаточный контейнер утверждений (claims) в формате `Base64Url(header).Base64Url(payload).Base64Url(signature)`.
Используется для:- JWS (подписанные токены — подлинность/целостность),
- JWE (зашифрованные токены — конфиденциальность),
- OIDC/OAuth2 как access/ID токены, а также service-to-service аутентификации.
Плюсы: автономность, кэшируемость, малые накладные расходы. Минусы: риск неправильной валидации, сложные кейсы отзыва.
2) Структура JWT
2.1 Заголовок (header, JSON)
Минимально: алгоритм и идентификатор ключа.
json
{ "alg": "ES256", "kid": "jwt-2025-10", "typ": "JWT" }
`alg`: алгоритм подписи/шифрования (RS256/ES256/PS256/HS256 и т. д.).
`kid`: указатель на ключ (для JWKS-ротации).
Опциональные источники ключей: `jku`, `x5u` (см. уязвимости §6.3).
2.2 Полезная нагрузка (payload, JSON)
Стандартизованные клеймы:- `iss` (issuer), `aud` (audience), `sub` (subject)
- `exp` (время истечения), `nbf` (не раньше), `iat` (выдан)
- `jti` (идентификатор токена, пригоден для отзывов)
- domain-клеймы: `scope/roles`, `tenant`, `kyc_level` и т. п.
2.3 Подпись (signature)
JWS = `sign(base64url(header) + "." + base64url(payload), private_key)`
Проверка: строго соответствующим публичным ключом и ровно тем алгоритмом, который сервер ожидает.
3) Базовые инварианты проверки
1. Алгоритм фиксируется конфигурацией ресурc-сервера (allow-list), а не доверяется содержимому `header.alg`.
2. Проверять `iss` и `aud` на точное совпадение, `exp/nbf` — с учетом небольшого `clock_skew` (±30–60s).
3. Отказывать токены без `kid` только если единственный ключ и нет ротации; иначе — требовать `kid`.
4. Не доверять никаким клеймам без авторизации на объектном уровне (BOLA-first).
5. Парсинг — после криптопроверки; базовые проверки размера до декодирования.
4) JWS vs JWE
JWS: подписан, но читаем. Не помещайте в payload PII/секреты.
JWE: шифрует payload; сложнее интеграция, ключевая модель критична.
В большинстве API достаточно JWS + запрет на чувствительные данные в payload.
5) Жизненный цикл токена
Access: короткий срок (5–30 минут).
Refresh: длительнее (7–30 дней), rotate-on-use (одноразовый), хранить «черный список» `jti/sid`.
Revocation: списки `jti` с TTL, интроспекция для opaque-токенов, сокращение `exp` при инцидентах.
Ротация ключей: JWKS с перекрытием (старый+новый), см. статью «Ротация ключей».
6) Частые уязвимости и как их закрывать
6.1 `alg = none` / подмена алгоритма
Суть: сервер доверяет полю `alg` и принимает неподписанный токен.
Защита: жесткий allow-list алгоритмов на сервере; отклонять `none` и неожиданные значения.
6.2 RS256→HS256 swap (симметризация)
Суть: злоумышленник подменяет `alg` на HS256 и использует публичный ключ как HMAC-секрет.
Защита: привязать ключ к алгоритму конфигурацией; не смешивать симметричные/асимметричные провайдеры в одном `kid`.
6.3 Инъекция ключей (`kid/jku/x5u`)
Сценарии:- `jku` указывает на контролируемый злоумышленником JWKS (подсунет свой ключ).
- `x5u`/`x5c` злоупотребление внешними сертификатами.
- `kid` с инъекцией пути/SQL (`"../../privkey.pem"` или `"' OR 1=1 --"`).
- Игнорировать удаленные `jku/x5u` либо фильтровать по строгому allow-list доменов.
- `kid` использовать только как ключ в локальном каталоге (таблица/кеш), без файловых путей/SQL конкатенаций.
- JWKS грузить с доверенных URL, короткий TTL, подпись/пинning канала.
6.4 Слабые секреты HS256 (брутфорс)
Суть: HMAC-секрет короткий/утек → подмена подписи.
Защита: использовать асимметрию (RS/ES/PS) или длину секрета ≥ 256 бит, секреты — только в KMS.
6.5 Отсутствующие/невалидные клеймы
Нет `aud`/`iss`/`exp` → токен кросс-сервисно пригоден или бесконечен.
Слишком длинный `exp` → риск компрометации.
Защита: требовать полный набор клеймов, `exp` короткий, `nbf`/`iat` валидировать с `clock_skew`.
6.6 Replay и кража токена
Суть: перехват/повтор токена (утечка в логах, XSS, MitM без TLS).
Защита:- TLS везде, `Secure`+`HttpOnly` cookie, SameSite=Lax/Strict.
- DPoP/PoP (привязка токена к клиентскому ключу) и/или mTLS для партнеров.
- Короткие `exp`, refresh-ротация, device-binding.
6.7 Утечки через XSS/хранилище
Суть: хранение JWT в `localStorage`/`sessionStorage` → доступен JS.
Защита: хранить access-токены в HttpOnly-cookie (если возможна cookie-модель) + строгая CSP/Trusted Types.
Для SPA без cookie — изолировать токен в памяти, минимально жить, защищать от XSS.
6.8 CSRF
Суть: при cookie-сессиях запросы от стороннего сайта.
Защита: SameSite, anti-CSRF токены (double submit), `Origin/Referer` проверка, Fetch-Metadata фильтры.
6.9 Oversize/злоупотребление размером
Суть: огромные payload/заголовки, DoS на парсинг.
Защита: лимиты размеров заголовков/тела, early-reject 431/413, фикс-набор клеймов.
6.10 Подмена `typ`/`cty`
Суть: путаница типов (`typ: "JWT"`), вложенные JOSE-объекты.
Защита: игнорировать `typ/cty` для безопасности, полагаться на фиксированные алгоритмы и схему клеймов.
7) Хранение и передача токенов
7.1 Серверные API (machine-to-machine)
mTLS/HMAC, желательно асимметрия для JWT, каналы — через mesh.
Хранилище ключей — KMS/HSM, ротация по расписанию, JWKS с перекрытием.
7.2 Браузерные клиенты
HttpOnly Secure Cookie для access/refresh; короткие TTL; обновление — через «silent refresh» с `SameSite=None` только под HTTPS.
Строгая CSP, Trusted Types, защитить от XSS; для SPA — по возможности не хранить токен на диске.
8) JWKS, ротация и отзыв
JWKS публикуется авторизатором; потребители кэшируют на 5–15 минут.
План ротации: добавить новый `kid` → начать им подписывать → через N дней удалить старый из JWKS → purge.
Отзыв: списки `jti/sid` c TTL; при инциденте временно сократить `exp` и форс-logout (инвалидировать refresh).
9) Multi-tenant и минимизация данных
Включать `tenant`/`org` в токен только если нужно по архитектуре; иначе подтягивать атрибуты из PDP по `sub`.
Никаких PII; минимальный набор клеймов → меньше риск утечек и корелляции.
10) Практический чек-лист валидации (сервер ресурса)
- Парсим только после проверки подписи и базовых лимитов размера.
- `alg` из конфигурации; отклонять неожиданные.
- Проверить `iss` ∧ `aud` ∧ `exp` ∧ `nbf` ∧ `iat` (с `clock_skew`).
- Проверить `kid` по локальному каталогу/JWKS (короткий TTL).
- Отфильтровать/нормализовать внешние указатели (`jku/x5u` — только allow-list).
- Ограничить длину/состав клеймов (схема).
- Применить объектную авторизацию на ресурс (BOLA-first).
- Логировать `kid`, `sub`, `aud`, `iss`, `jti`, `exp`, `tenant`, `trace_id` (без PII).
- Метрики ошибок подписи, просрочек, аудита ротации.
11) Примеры безопасных политик (псевдо)
11.1 Конфигурация ожиданий алгоритма
yaml jwt:
expected_issuer: "https://auth. example. com"
expected_audience: ["wallet-service"]
allowed_algs: ["ES256"] # fix the jwks_url: "https ://auth. example. com/.well-known/jwks. json"
jwks_cache_ttl: 600s clock_skew: 60s required_claims: ["iss","aud","sub","exp","iat"]
11.2 Отказ от удаленных `jku/x5u`
yaml reject_untrusted_key_sources: true allowed_jku_hosts: ["auth. example. com"] # if absolutely necessary
11.3 Пример списка отзывов (Redis)
pseudo if redis. exists("revoke:jti:" + jti) then deny()
if now() > exp then deny()
12) Наблюдаемость и форензика
Метрики: `jwt_verify_fail_total{reason}`, `jwt_expired_total`, `jwks_refresh_total`, доля `kid`.
Логи (структурированные): `iss/aud/sub/kid/jti/exp/tenant/trace_id`, причина отказа.
Дашборды: «истекающие скоро», всплеск ошибок валидации, распределение по регионам/клиентам.
Алерты: рост `verify_fail` (подпись/алгоритм), ошибок JWKS, доли просроченных токенов.
13) Антипаттерны
Доверять `alg` из токена; поддерживать `none`.
Долгоживущие access-токены и отсутствие refresh-ротации.
HS256 с коротким секретом/секрет в ENV без KMS.
Принимать `jku/x5u` с любого домена; динамически загружать JWKS без allow-list.
Класть PII/секреты в payload JWS.
Хранить токены в `localStorage` при наличии XSS-рисков.
Заглушать ошибки валидации (возвращать 200 с «soft error»).
Отсутствие BOLA-проверок и полагание только на `scope/role`.
14) Специфика iGaming/финансов
Клеймы: `kyc_level`, `risk_tier`, `tenant`, строгие `aud`.
Короткие TTL для write-операций (депозиты/выводы), PoP/DPoP или mTLS для критичных маршрутов.
Регуляторный аудит: неизменяемые журналы входов/отказов, хранение логов в границах региона.
У партнеров/PSP — отдельные ключи/`aud`, пер-тенант ключи и отдельные JWKS.
15) Чек-лист prod-готовности
- Жесткий allow-list алгоритмов; `none` запрещен.
- `iss/aud/exp/nbf/iat/jti` проверяются; короткие `exp`.
- JWKS с перекрытием, короткий TTL кэша; мониторинг долей `kid`.
- Refresh — rotate-on-use; списки `jti/sid` с TTL; плейбук отзывов.
- PoP/DPoP или mTLS на критичных маршрутах.
- HttpOnly-cookies для браузера, CSP/Trusted Types против XSS; CSRF-защита.
- Без PII в payload; минимальный набор клеймов.
- Метрики/логи по валидации и отказам; алерты JWKS/verify_fail.
- Тесты негативных сценариев: RS→HS swap, `kid`-инъекции, `jku` spoofing, oversize, clock-skew.
16) TL;DR
Фиксируйте алгоритм и ключи на стороне сервера, не доверяйте `alg` из токена, требуйте `iss/aud/exp/nbf/iat/jti`. Ротируйте ключи через JWKS с перекрытием, держите токены короткоживущими, а refresh — одноразовыми. Храните токены безопасно (HttpOnly-cookie для веба), минимизируйте клеймы, не кладите PII. Закрывайте `jku/x5u/kid`-векторы, избегайте HS256 с слабыми секретами, добавляйте PoP/DPoP или mTLS на критичных путях и всегда делайте BOLA-проверки на уровень ресурса.