Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Moscow Python Meetup №110. Адриан Макриденко (Г...

Moscow Python Meetup №110. Адриан Макриденко (ГК Астра Линукс, разработчик серверной части). Токены авторизации: почему JWT легко использовать неправильно и как это исправляет PASETO?

Почему JWT часто используют небезопасно, какие проблемы это создаёт и есть ли реальные альтернативы? Подробно разберём устройство PASETO и обсудим, почему его подход может быть более надёжным.

Видео: https://moscowpython.ru/meetup/110/authorization-tokens/

Moscow Python: http://moscowpython.ru
Курсы Learn Python: http://learn.python.ru
Moscow Python Podcast: http://podcast.python.ru
Заявки на доклады: https://bit.ly/mp-speaker

Avatar for Moscow Python Meetup

Moscow Python Meetup PRO

April 20, 2026

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. Что такое JWT? JWT (JSON Web Token) — компактный URL-безопасный

    токен для передачи утверждений (claims) между сторонами Стандартизован в RFC 7519 3
  2. Структура JWT: Header { "alg": "HS256", "typ": "JWT" } Кодируется

    base64url Содержит: • alg — алгоритм подписи/шифрования (HS256, RS256, ES256, none...) • typ — тип токена (опционально) 6
  3. Структура JWT: Payload { "sub": "1234567890", "name": "John Doe", "admin":

    true, "iat": 1516239022, "exp": 1516242622 } Зарезервированные claims (RFC 7519): • iss — издатель • sub — субъект • aud — аудитория • exp — время истечения • nbf — не раньше • iat — время выпуска • jti — ID токена 7
  4. Структура JWT: Signature HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret

    ) Подписывается: • Симметрично (HS256, HS384, HS512) — общий секрет • Асимметрично (RS256, RS384, RS512, ES256...) — приватный ключ Или НЕ подписывается вообще ( alg: none ) 8
  5. Семейство JOSE JOSE (JSON Object Signing and Encryption) — совокупность

    стандартов IETF: Стандарт Назначение JWS Формат подписанных сообщений JWE Формат шифрованных сообщений JWK Описание ключей (JSON) JWA Набор криптоалгоритмов JWT Собственно токен JWT — лишь часть семейства JOSE 9
  6. Как используется JWT Нормальный сценарий: 1. Сервер выпускает JWT и

    подписывает его 2. Клиент сохраняет токен 3. На каждый запрос отправляет JWT 4. Сервер проверяет подпись и exp 10
  7. Случай 1: Простой токен без подписи { "alg": "none", "typ":

    "JWT" } • Нет подписи • Нет никаких обязательных полей • Полностью соответствует RFC 7519 14
  8. Случай 2: Максимальный JWS+JWE+JWK { "alg": "RS256", "typ": "JWT", "kid":

    "key-id", "jku": "https://server/keys.json", "x5u": "https://server/cert.pem", "iss": "https://auth.example.com", "sub": "1234567890", "aud": ["https://api.example.com"], "exp": 1516239022, "nbf": 1516239022, "iat": 1516239022, "jti": "unique-token-id" } • JWS с RS256 • Все зарезервированные claims • Дополнительные поля идентификации ключей Оба токена полностью валидны по спецификации! 15
  9. Другие «ловушки»: • Избыточная гибкость — алгоритм указывается в токене

    • Необязательность проверки — можно забыть проверить • Много опций — alg: none , разные алгоритмы, ключи • Сложность спецификации — большая и многослойная Спецификация JOSE даёт широкие возможности, которые легко неправильно использовать 16
  10. Проблема 1: Алгоритм none Спецификация позволяет задать "alg":"none" — отсутствие

    подписи { "alg": "none", "typ": "JWT" } Известная уязвимость: • Если сервер НЕ проверяет этот параметр • Или библиотека некорректно обрабатывает • Злоумышленник просто указывает "alg":"none" и модифицирует содержимое без подписи Многие ранние реализации принимали такие токены как валидные 17
  11. Алгоритм none: Пример атаки # Токен с alg: none header

    = {"alg": "none", "typ": "JWT"} payload = {"sub": "admin", "admin": true} # Злоумышленник меняет payload payload = {"sub": "admin", "admin": true, "role": "superadmin"} # Сервер принимает токен без подписи! # Все ранние версии PyJWT, node-jws и др. были уязвимы Несмотря на фиксы в обновлённых библиотеках, уязвимость встречается в устаревшем ПО 18
  12. Проблема 2: Конфуз алгоритмов Key Confusion Attack — атака на

    совмещение ключей JWT поддерживает: • Симметричные (HS256) — HMAC • Асимметричные (RS256, ES256) — RSA, ECDSA Некоторые библиотеки при верификации опираются на: • Поле alg из токена • Переданный «ключ» без жёсткой проверки типа 19
  13. Конфуз алгоритмов: Как работает Сервер ожидает alg=RS256 ↓ Передаёт RSA-ключ

    в verify() ↓ Злоумышленник создаёт токен с alg=HS256 ↓ Подписывает токен ОТКРЫТЫМ RSA-ключом (как HMAC-секрет) ↓ Сервер проверяет HMAC с "секретом" (открытый ключ) ↓ Принимает поддельный токен! Результат: злоумышленник подделывает права без знания приватного ключа 20
  14. Конфуз алгоритмов: Пример кода # Уязвимый код (Python, PyJWT) import

    jwt public_key = load_rsa_public_key("server.pem") # Злоумышленник передаёт token с alg: HS256 # и подписывает открытым ключом как HMAC-секретом token = jwt.encode( {"sub": "admin"}, public_key, # ← использует открытый ключ как секрет! algorithm="HS256" ) # Сервер проверяет jwt.decode(token, public_key, algorithms=["RS256", "HS256"]) # ← УЯЗВИМОСТЬ: принимает HS256 с публичным ключом! 21
  15. Проблема 3: Неправильная верификация Самая грубая ошибка — просто не

    проверять подпись // Node.js - не делайте так! const token = req.headers.authorization.split(' ')[1]; // decode() ТОЛЬКО раскодирует, не проверяет подпись! const decoded = jwt.decode(token); console.log(decoded.admin); // true - но токен не проверен! OWASP: если использовать decode вместо verify , можно читать данные без проверки подписи — это прямая уязвимость 22
  16. Проблема 4: Сложность JOSE Спецификация JOSE большая и многослойная Проблемные

    поля: • kid — идентификатор ключа • jku — URL ключа • x5u — URL сертификата • x5c — цепочка сертификатов Неверная обработка позволяет проводить атаки типа подмены ключей JWE (шифрование) — детали сложны, редко корректно применяются Массив возможностей → разработчики невольно допускают уязвимость 23
  17. Проблема 5: Отзыв токенов JWT изначально спроектированы как «самодостаточные» (stateless)

    • Верификация = проверка подписи + сроков • Если надо отозвать досрочно → нужны чёрные списки • Иначе токен «живёт» до конца срока JWT – не то же самое, что «сессионный токен» JWT — заявленные утверждения (claims). Если хранить состояние, возникают сложности с отзывом прав, «разлогином» 24
  18. Как и где JWT используют неправильно JWT как сессионный токен

    Многие воспринимают JWT как замену серверной сессии Но JWT статичны: • Сам токен содержит все данные • Верифицируется только локально по подписи • После выдачи не знает об изменениях прав Результат: • «Неотзываемые» сессии • Невозможность реализовать logout • Админ изменил права → JWT сам не узнает 25
  19. Неправильное использование: Секреты в payload { "sub": "1234567890", "email": "[email protected]",

    "password_hash": "xxx", // ОШИБКА! "api_key": "secret-key" // ОШИБКА! } Поскольку тело JWT обычно не шифруется (JWS), payload открыт любому, у кого есть токен Payload JWT = открытый текст (base64). Любой может декодировать и прочитать! 26
  20. Неправильное использование: Слабые ключи # Плохо: короткий ключ secret =

    "123456" # X Легко подобрать! # Хорошо: длинный случайный import secrets secret = secrets.token_bytes(32) # 256 бит Или берутся неподходящие ключи RSA (PKCS#1 v1.5), которые считаются менее безопасными Разработчик может просто указать любой алгоритм — спецификация не мешает 27
  21. Неправильное использование: Обработка ошибок # ОШИБКА: просто пропустить ошибку try:

    payload = jwt.decode(token, key) except Exception: # Пропускаем — принимаем любой кривой токен! payload = jwt.decode(token, options={"verify_signature": False}) Если сервер не может проверить подпись (нет ключа) — «просто пропустить» и вернуть payload OWASP: запрет любых JWT, для которых не прошла верификация подписи 28
  22. Неправильное использование: Не по HTTPS // ОШИБКА: отправка не по

    HTTPS localStorage.setItem('token', jwt); // Злоумышленник перехватывает токен в сети // Подпись бессмысленна — токен уже украден! Несмотря на общие рекомендации, практикуется отправка в незащищённом канале 29
  23. Резюме: Типичные ошибки 1. JWT как сессия — неотзываемые токены

    2. Секреты в payload — открытый JSON 3. Слабые ключи — легко подобрать 4. Не verify — decode вместо verify 5. Не по HTTPS — перехват токена 6. Сложная схема — JWS + JWE + JWK Частая «ошибка» — выбор JWT «по привычке», без анализа требований безопасности 30
  24. Неочевидные проблемы JWT Refresh токенов JWT не предусматривает обновления без

    выдачи нового токена exp встроен внутрь токена → затрудняет ротацию ключей или выдачу «нового» токена без потери сессии 31
  25. Claims Зарезервированные поля ( iss , sub , aud ,

    exp ) не обязательны Разработчики полагаются только на exp , забывая проверять iss и aud Результат: сервер может принять чужой JWT от другого издателя 32
  26. Управление ключами { "jku": "https://attacker.com/keys.json", "kid": "unique-key-id" } Код должен:

    1. Найти нужный ключ (по kid или URL) 2. Загрузить его 3. Распарсить Ошибки в этих шагах привели к атакам — клиент мог указать свою jku 33
  27. Размер токена JWT часто хранятся в HTTP-заголовках или куки •

    Base64 увеличивает размер • Payload — JSON-текст • Токены занимают сотни байт Может вызвать проблемы с размером заголовков 34
  28. Избыточная гибкость Многие проблемы JWT связаны с «избыточной гибкостью»: •

    Разработчик сам выбирает алгоритмы и форматы • Может ошибиться • Сложно проверить правильность реализации PASETO спроектирован, чтобы «не дать разработчику прострелить ногу» 35
  29. Что такое PASETO? PASETO (Platform-Agnostic SEcurity TOkens) — стандарт токенов

    от Paragon Initiative Enterprises Главная идея: предоставить замену JWT, в которой «из коробки» используются только безопасные современные алгоритмы Разработчик лишён возможности выбрать «бессмысленные» варианты 37
  30. Формат PASETO v4.local.eyJ... ← шифрование (симметричный ключ) v4.public.eyJ... ← подпись

    (асимметричный ключ) Структура: v{версия}.{режим}.{тело}.{footer?} • version — v1, v2, v3, v4 • режим — local (шифрование) или public (подпись) • footer — опционально 38
  31. Версии PASETO Версия Local (шифрование) Public (подпись) v1 AES-GCM RS256,

    ES256 v2 AES-GCM RS256, ES256 v3 AES-GCM EdDSA, ECDSA v4 XChaCha20-Poly1305 Ed25519 В v4 используются современные алгоритмы: • XChaCha20-Poly1305 — AEAD шифрование • Ed25519 — Edwards-curve Digital Signature 39
  32. Ключевые отличия от JWT Нет поля alg Алгоритм определяется версией,

    а не указывается в токене Злоумышленник не может изменить алгоритм Нет alg: none В PASETO такой атаки просто нет Фиксированные алгоритмы Каждой версии соответствует набор алгоритмов — разработчик не выбирает 40
  33. Отличия: Безопасность по умолчанию JWT: • Можно выбрать «несекурный» алгоритм

    • alg: none — классическая уязвимость • Гибкость → много возможностей ошибиться PASETO: • Нельзя выбрать слабый алгоритм • Нет alg: none • Минимум конфигурации 41
  34. Отличия: Простота JWT: • JWS + JWE + JWK •

    Много необязательных полей • Сложная спецификация PASETO: • Только header + payload + optional footer • Нет kid/jku/jwk • Чистая спецификация 42
  35. Отличия: Прозрачность Структура токена очевидна по префиксу: v4.local. ← симметричное

    шифрование v4.public. ← асимметричная подпись Не требуется дополнительная информация о ключах 43
  36. PASETO: local режим (шифрование) from jam.paseto.v4 import PASETOv4 import secrets

    # Генерируем 32-байтовый секрет secret_key = secrets.token_bytes(32) # Создаём PASETO для шифрования paseto = PASETOv4.key("local", secret_key) # Кодируем token = paseto.encode({ "user": "meowl", "exp": "2030-01-01T00:00:00+00:00" }) # Декодируем payload, _ = paseto.decode(token) 44
  37. Как работает local режим 1. Формируется заголовок v4.local. 2. Генерируется

    24-байтовый случайный nonce (для XChaCha20-Poly1305) 3. Собирается AAD (additional authenticated data) 4. Вызывается xchacha20poly1305_encrypt(secret, nonce, payload, aad) 5. Токен = header + base64(nonce + ciphertext) AEAD — шифрование + аутентификация вместе При верификации: если данные повреждены → ошибка 45
  38. PASETO: public режим (подпись) from jam.paseto.v4 import PASETOv4 from cryptography.hazmat.primitives.asymmetric.ed25519

    import Ed25519PrivateKey # Генерируем ED25519 ключи private_key = Ed25519PrivateKey.generate() # Создаём PASETO для подписи paseto = PASETOv4.key("public", private_key) # Кодируем token = paseto.encode({"user": "mewfish"}) # Декодируем (нужен публичный ключ) payload, _ = paseto.decode(token, public_key) 46
  39. Как работает public режим 1. Формируется заголовок v4.public. 2. Конкатенируется

    с JSON-payload 3. Формируется pre_auth = PAE([header, payload, footer]) 4. Приватный ключ Ed25519 подписывает pre_auth 5. Подпись: 64 байта При декодировании: • Проверяется префикс v4.public. • Проверяется длина подписи (64 байта) • Проверяется подпись публичным ключом 47
  40. Почему это безопаснее? 1. Нет поля alg — версия определяет

    алгоритм 2. Фиксированные алгоритмы — XChaCha20-Poly1305, Ed25519 3. AEAD — шифрование + аутентификация вместе 4. Жёсткие проверки — префикс, длина, подпись Злоумышленник не может: • Изменить алгоритм • Указать alg: none • Использовать слабый ключ 48
  41. Сравнение: JWT vs PASETO Характеристика JWT PASETO Архитектура JWS/JWE с

    alg Версионированные протоколы Алгоритм Пользователь выбирает Фиксирован: XChaCha20, Ed25519 alg:none Да (уязвимость) Нет key confusion Да Нет jku/kid Да Нет Управление ключами Сложное (JWK) Простое Размер Больше Меньше 50
  42. Сравнение: Гибкость vs Безопасность JWT: • Максимальная гибкость • Много

    возможностей ошибиться • Разработчик сам выбирает PASETO: • Жёсткие безопасные умолчия • Минимум гибкости • Версия = алгоритм PASETO: «лучше использовать правильно, чем дать свободу на выбор алгоритмов» 51
  43. Важное уточнение PASETO не решает все проблемы! • Не предусматривает

    механизмы предотвращения replay-атак • Задачи «stateless-сессий» — вне его ответственности • Для этого всё равно нужны серверные слежения PASETO просто гарантирует криптостойкость при передаче данных 52
  44. Экосистема JWT: • Поддерживается всеми фреймворками • OpenID Connect, OAuth2

    • Готовые клиенты everywhere PASETO: • Молодой стандарт • Реализации на многих языках растут • Python: jam.paseto , paseto , pyseto 53
  45. Пример: Замена JWT на PASETO Было (JWT): token = jwt.encode(payload,

    secret, algorithm="HS256") Стало (PASETO): paseto = PASETOv4.key("local", secret) token = paseto.encode(payload) Библиотека сама выбирает правильный алгоритм — разработчик не ошибается 54
  46. Когда выбирать PASETO? Использовать PASETO если: • Нужен самодостаточный токен

    • Важна безопасность «из коробки» • Хотите минимизировать риски • Не нужна сложная интеграция Использовать JWT если: • Требуется совместимость • Уже есть инфраструктура • Нужен OIDC/OAuth2 55
  47. Заключение JWT — зрелый стандарт, но: • Легко использовать неверно

    • Большая ответственность на разработчике • Много «подводных камней» PASETO — безопасная альтернатива: • Фиксированные алгоритмы • Минимум конфигурации • Сложно использовать неправильно 56
  48. Рекомендации 1. Критически оценивать необходимость JWT ◦ Если нужен простейший

    маркер → сессия с хранением на сервере 2. Если используете JWT: ◦ Фиксировать алгоритмы ◦ Периодически ротировать ключи ◦ Проверять все поля ( iss , aud , exp ) ◦ Использовать verify , не decode 3. Если нужен самодостаточный токен: ◦ Предпочесть PASETO ◦ Или строго контролировать JWT 57