Slide 1

Slide 1 text

Токены авторизации: почему JWT легко использовать неправильно и как это исправляет PASETO? 1

Slide 2

Slide 2 text

Адриан Макриденко Разработчик серверных компонентов в ГК Астра. Пишу на Python, делаю штуки 2

Slide 3

Slide 3 text

Что такое JWT? JWT (JSON Web Token) — компактный URL-безопасный токен для передачи утверждений (claims) между сторонами Стандартизован в RFC 7519 3

Slide 4

Slide 4 text

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik pvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6M TUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 4

Slide 5

Slide 5 text

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik pvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6M TUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 5

Slide 6

Slide 6 text

Структура JWT: Header { "alg": "HS256", "typ": "JWT" } Кодируется base64url Содержит: • alg — алгоритм подписи/шифрования (HS256, RS256, ES256, none...) • typ — тип токена (опционально) 6

Slide 7

Slide 7 text

Структура 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

Slide 8

Slide 8 text

Структура JWT: Signature HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) Подписывается: • Симметрично (HS256, HS384, HS512) — общий секрет • Асимметрично (RS256, RS384, RS512, ES256...) — приватный ключ Или НЕ подписывается вообще ( alg: none ) 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Как используется JWT Нормальный сценарий: 1. Сервер выпускает JWT и подписывает его 2. Клиент сохраняет токен 3. На каждый запрос отправляет JWT 4. Сервер проверяет подпись и exp 10

Slide 11

Slide 11 text

АЧО ЕСЛИ НЕ ЖЭ ВЕ ТЕ? 11

Slide 12

Slide 12 text

Но где ловушки? Уже на этом уровне скрываются «ловушки»: 12

Slide 13

Slide 13 text

Проблема спецификации: Два крайних случая JWT спецификация не обязывает использовать подпись или шифрование 13

Slide 14

Slide 14 text

Случай 1: Простой токен без подписи { "alg": "none", "typ": "JWT" } • Нет подписи • Нет никаких обязательных полей • Полностью соответствует RFC 7519 14

Slide 15

Slide 15 text

Случай 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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Алгоритм 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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Конфуз алгоритмов: Пример кода # Уязвимый код (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

Slide 22

Slide 22 text

Проблема 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

Slide 23

Slide 23 text

Проблема 4: Сложность JOSE Спецификация JOSE большая и многослойная Проблемные поля: • kid — идентификатор ключа • jku — URL ключа • x5u — URL сертификата • x5c — цепочка сертификатов Неверная обработка позволяет проводить атаки типа подмены ключей JWE (шифрование) — детали сложны, редко корректно применяются Массив возможностей → разработчики невольно допускают уязвимость 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Как и где JWT используют неправильно JWT как сессионный токен Многие воспринимают JWT как замену серверной сессии Но JWT статичны: • Сам токен содержит все данные • Верифицируется только локально по подписи • После выдачи не знает об изменениях прав Результат: • «Неотзываемые» сессии • Невозможность реализовать logout • Админ изменил права → JWT сам не узнает 25

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Неправильное использование: Не по HTTPS // ОШИБКА: отправка не по HTTPS localStorage.setItem('token', jwt); // Злоумышленник перехватывает токен в сети // Подпись бессмысленна — токен уже украден! Несмотря на общие рекомендации, практикуется отправка в незащищённом канале 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Неочевидные проблемы JWT Refresh токенов JWT не предусматривает обновления без выдачи нового токена exp встроен внутрь токена → затрудняет ротацию ключей или выдачу «нового» токена без потери сессии 31

Slide 32

Slide 32 text

Claims Зарезервированные поля ( iss , sub , aud , exp ) не обязательны Разработчики полагаются только на exp , забывая проверять iss и aud Результат: сервер может принять чужой JWT от другого издателя 32

Slide 33

Slide 33 text

Управление ключами { "jku": "https://attacker.com/keys.json", "kid": "unique-key-id" } Код должен: 1. Найти нужный ключ (по kid или URL) 2. Загрузить его 3. Распарсить Ошибки в этих шагах привели к атакам — клиент мог указать свою jku 33

Slide 34

Slide 34 text

Размер токена JWT часто хранятся в HTTP-заголовках или куки • Base64 увеличивает размер • Payload — JSON-текст • Токены занимают сотни байт Может вызвать проблемы с размером заголовков 34

Slide 35

Slide 35 text

Избыточная гибкость Многие проблемы JWT связаны с «избыточной гибкостью»: • Разработчик сам выбирает алгоритмы и форматы • Может ошибиться • Сложно проверить правильность реализации PASETO спроектирован, чтобы «не дать разработчику прострелить ногу» 35

Slide 36

Slide 36 text

Переходим к PASETO 36

Slide 37

Slide 37 text

Что такое PASETO? PASETO (Platform-Agnostic SEcurity TOkens) — стандарт токенов от Paragon Initiative Enterprises Главная идея: предоставить замену JWT, в которой «из коробки» используются только безопасные современные алгоритмы Разработчик лишён возможности выбрать «бессмысленные» варианты 37

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Версии 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

Slide 40

Slide 40 text

Ключевые отличия от JWT Нет поля alg Алгоритм определяется версией, а не указывается в токене Злоумышленник не может изменить алгоритм Нет alg: none В PASETO такой атаки просто нет Фиксированные алгоритмы Каждой версии соответствует набор алгоритмов — разработчик не выбирает 40

Slide 41

Slide 41 text

Отличия: Безопасность по умолчанию JWT: • Можно выбрать «несекурный» алгоритм • alg: none — классическая уязвимость • Гибкость → много возможностей ошибиться PASETO: • Нельзя выбрать слабый алгоритм • Нет alg: none • Минимум конфигурации 41

Slide 42

Slide 42 text

Отличия: Простота JWT: • JWS + JWE + JWK • Много необязательных полей • Сложная спецификация PASETO: • Только header + payload + optional footer • Нет kid/jku/jwk • Чистая спецификация 42

Slide 43

Slide 43 text

Отличия: Прозрачность Структура токена очевидна по префиксу: v4.local. ← симметричное шифрование v4.public. ← асимметричная подпись Не требуется дополнительная информация о ключах 43

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Как работает 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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Почему это безопаснее? 1. Нет поля alg — версия определяет алгоритм 2. Фиксированные алгоритмы — XChaCha20-Poly1305, Ed25519 3. AEAD — шифрование + аутентификация вместе 4. Жёсткие проверки — префикс, длина, подпись Злоумышленник не может: • Изменить алгоритм • Указать alg: none • Использовать слабый ключ 48

Slide 49

Slide 49 text

NoneAlg Confuse DecodeBreach ForgedJWT ExposedData Compromise JWT ForgeAttempt Rejected PASETO 49

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Важное уточнение PASETO не решает все проблемы! • Не предусматривает механизмы предотвращения replay-атак • Задачи «stateless-сессий» — вне его ответственности • Для этого всё равно нужны серверные слежения PASETO просто гарантирует криптостойкость при передаче данных 52

Slide 53

Slide 53 text

Экосистема JWT: • Поддерживается всеми фреймворками • OpenID Connect, OAuth2 • Готовые клиенты everywhere PASETO: • Молодой стандарт • Реализации на многих языках растут • Python: jam.paseto , paseto , pyseto 53

Slide 54

Slide 54 text

Пример: Замена JWT на PASETO Было (JWT): token = jwt.encode(payload, secret, algorithm="HS256") Стало (PASETO): paseto = PASETOv4.key("local", secret) token = paseto.encode(payload) Библиотека сама выбирает правильный алгоритм — разработчик не ошибается 54

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Заключение JWT — зрелый стандарт, но: • Легко использовать неверно • Большая ответственность на разработчике • Много «подводных камней» PASETO — безопасная альтернатива: • Фиксированные алгоритмы • Минимум конфигурации • Сложно использовать неправильно 56

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

makridenko.ru Jam 58