Безопасность JWT: атаки, защита и чек-лист 2026 года
На JWT держится почти вся современная аутентификация, но практики безопасности, которые делают токены надёжными, пропускают куда чаще, чем стоило бы. JWT стал стандартным форматом учётных данных для OAuth 2.0, OpenID Connect и вызовов между сервисами внутри микросервисной архитектуры. Из года в год он же даёт поток CVE, и почти все они сводятся к одним и тем же ошибкам, которых легко избежать: приём неподписанных токенов, доверие к алгоритму, выбранному атакующим, слабый секрет подписи или пропуск проверки claims.
JWT безопасен, когда одновременно выполняются четыре условия. Подпись цела, алгоритм нельзя подменить атакующему, claims действительно проверяются, а сам токен хранится там, откуда его не украсть мимоходом. Нарушьте хотя бы одно условие, и вместо защищённого API вы получите обход аутентификации. Ниже разобраны три самые важные атаки, а затем защита: выбор и фиксация алгоритма, управление ключами, проверка claims и хранение токенов. В конце есть чек-лист, который можно вставить прямо в ревью.
Как подпись JWT на самом деле вас защищает (и от чего нет)
Сначала важно усвоить одно: JWT кодируется, а не шифруется. Подписанный токен состоит из трёх сегментов Base64URL, соединённых точками: header.payload.signature. Заголовок и полезная нагрузка — это обычный Base64URL от JSON. Любой, у кого есть токен, может прочитать каждый claim внутри. Вставьте токен в наш декодер JWT, и вы увидите заголовок и полезную нагрузку как читаемый JSON, без всякого ключа. Полезная нагрузка по сути публична.
Откуда же тогда берётся безопасность? Из подписи, и только из неё. Это криптографическое значение, вычисленное над заголовком и полезной нагрузкой с помощью секрета (HMAC) или закрытого ключа (RSA, ECDSA). Атакующий может свободно прочитать токен, но без ключа подписи не создаст другой токен, который пройдёт проверку. На этом одном свойстве и держится вся модель доверия.
Отсюда два следствия. Во-первых, не кладите в полезную нагрузку секреты — пароли, API-ключи, полные персональные данные: их прочитает любой, кто перехватит токен. Во-вторых, вся ваша защита держится на одном шаге, на корректной проверке подписи. По нему атакующие и бьют. Если хотите разобрать чтение токена сегмент за сегментом, посмотрите как декодировать JWT.
3 критические атаки на JWT (и как остановить каждую)
Большинство уязвимостей JWT — вариации на одну тему: сервер доверяет тому, чем управляет атакующий. Разберём три, которые ломают аутентификацию напрочь, с механизмом каждой и способом устранения.
1. Атака alg:none — обход через неподписанный токен
Спецификация JWS включает значение alg, равное none, то есть «без подписи». Токен с alg:none имеет пустой сегмент подписи и всё равно заканчивается завершающей точкой, как header.payload.. Атака проста: берём валидный токен, меняем alg в заголовке на none, подставляем любые нужные claims (скажем, "role": "admin") и убираем подпись. Ранние библиотеки JWT принимали такое по умолчанию, и подделанный токен спокойно проходил проверку. Ни ключа, ни подписи, а на выходе полное выдавание себя за другого.
Как выглядит такой токен, видно, если загрузить пример «alg:none» в наш декодер JWT: он показывает явное красное предупреждение, что токен не подписан и принимать его для аутентификации нельзя. Собрать такой токен самому — упражнение на пару минут, и оно хорошо проясняет угрозу.
Защищает явный список разрешённых алгоритмов при каждом вызове проверки. Не отдавайте решение о том, что допустимо, на откуп настройкам библиотеки по умолчанию: прежние умолчания были снисходительны, а явное указание стоит вам одной дополнительной опции.
// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);
// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });
Значение none не должно появляться в этом массиве никогда. Если ваша библиотека не умеет фиксировать алгоритмы, замените её.
2. Путаница алгоритмов — RS256 понижается до HS256
На практике это самая опасная уязвимость JWT, известная с 2015 года и до сих пор всплывающая в аудитах. Она бьёт по серверам, которые решают, как проверять, исходя из поля alg в заголовке, то есть из той части токена, которую атакующий может переписать.
Механизм такой. Ваш сервер выпускает токены RS256: подписывает закрытым ключом RSA и проверяет соответствующим открытым ключом. Этот открытый ключ по определению публичен, он может лежать в вашем JWKS-эндпоинте или в репозитории. Атакующий берёт его, меняет заголовок токена с RS256 на HS256 и подписывает поддельную полезную нагрузку через HMAC-SHA256, используя строку открытого ключа как секрет HMAC. Дальше сторона проверки: если ваш код читает alg из заголовка и выбирает HMAC, он вычисляет HMAC-SHA256 над токеном с тем же открытым ключом в роли секрета. Подписи совпадают. Поддельный токен принят.
В корне проблемы столкновение двух фактов: проверяющая сторона доверилась заголовку alg, которым управляет атакующий, а открытый ключ RSA оказался доступен атакующему для применения как ключ HMAC. Ни один из этих фактов сам по себе не баг. Открытый ключ и должен быть публичным, а заголовок alg и должен описывать токен. Уязвимость рождается там, где ваша логика проверки позволяет этому заголовку выбирать тип ключа и алгоритм: тогда значение, которое пишет атакующий, задаёт криптографический путь, который выполняет сервер.
// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);
// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Фиксируйте асимметричный алгоритм явно (только RS256 или ES256), держите проверку HMAC на полностью отдельном пути кода от проверки RSA и берите поддерживаемую библиотеку, которая различает типы ключей. Наш декодер JWT помечает любой токен семейства HS предупреждением о путанице с открытым ключом именно потому, что эта атака так распространена: если токен, который вы ждали асимметричным, оказался HS256, это предупреждение и есть ваш сигнал.
3. Слабый секрет HMAC — атаки перебором и по словарю
Если вы используете HMAC (HS256/384/512), вся безопасность токена держится на энтропии одного секрета. Когда секрет короткий, словарное слово или что-то вроде secret или password123, атакующий, перехвативший единственный валидный токен, взломает его офлайн. Инструменты вроде hashcat перебирают миллиарды кандидатов в секунду против подписи токена. Как только секрет вскрыт, атакующий выпускает любые токены и получает валидные права admin навсегда.
Эта атака опасна ещё и тем, что идёт целиком офлайн. Атакующий не долбит ваш эндпоинт входа, так что ни ограничение частоты, ни записи в логах не сработают. Он перехватывает один токен, взламывает секрет на своём железе и возвращается, лишь когда уже может подписывать токены, проходящие любую вашу проверку. Исправление здесь не обсуждается: берите минимум 32 случайных байта (256 бит) из криптографически стойкого источника и держите секрет в менеджере секретов, а не в коде и не в репозитории.
// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";
// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');
Нужно быстро получить стойкое значение? Наш генератор паролей выдаёт строки с высокой энтропией, которые годятся как ключ HMAC. А чтобы попробовать на практике, подпишите тестовый токен стойким секретом в нашем энкодере JWT: он работает целиком в браузере, так что секрет никогда не покидает вашу машину. Когда же проверка пересекает границу доверия (несколько сервисов, сторонние проверяющие), откажитесь от HS256 в пользу асимметричного алгоритма, о чём дальше.
Выбор и фиксация подходящего алгоритма
Именно на выборе алгоритма атака путаницы выигрывается или проигрывается, поэтому выбирайте осознанно. Три варианта, которыми вы реально будете пользоваться:
| Алгоритм | Тип | Ключ подписи / проверки | Когда применять |
|---|---|---|---|
| HS256 | Симметричный (HMAC) | Один общий секрет | Одна граница доверия, одна сторона подписывает и проверяет |
| RS256 | Асимметричный (RSA) | Закрытый ключ подписывает / открытый проверяет | Межсервисное взаимодействие, сторонняя проверка, ротация JWKS |
| ES256 | Асимметричный (ECDSA) | Закрытый ключ подписывает / открытый проверяет | То же, что RS256, ключи меньше и быстрее — предпочтительно для новых систем |
Правило короткое. Если одна и та же сторона подписывает и проверяет внутри одной границы доверия, HS256 хорош и быстр. Если проверять должен кто-то ещё (другой сервис, партнёр, публичный клиент), берите асимметричный алгоритм, и лучше ES256: его ключи и подписи намного меньше, чем у RSA, при равной стойкости. Образцовые токены HS256, RS256 и ES256 можно подписать бок о бок в энкодере JWT и сравнить их структуру и длину подписи.
Что бы вы ни выбрали, главная защита одна: зафиксируйте один явный набор алгоритмов при вызове проверки и никогда не доверяйте полю alg в заголовке. На этом списке разрешённых держится всё остальное.
Управление ключами и их ротация
Алгоритмы безопасны ровно настолько, насколько безопасны стоящие за ними ключи, а на обращении с ключами большинство руководств умолкает. Для HS256 секрет — минимум 32 случайных байта, и живёт он в менеджере секретов: AWS Secrets Manager, HashiCorp Vault или Azure Key Vault. Для асимметричных алгоритмов закрытый ключ хранится в HSM или KMS и никогда не касается кода приложения; открытый ключ обычно публикуется через JWKS-эндпоинт, который запрашивают проверяющие.
Ротация должна быть рутиной, а не авралом. Помечайте каждый ключ идентификатором kid (key ID) в заголовке JWT, чтобы проверяющие знали, какой ключ подписал тот или иной токен. На стороне проверки держите небольшой набор валидных ключей — текущий плюс недавний предыдущий, — чтобы токены, подписанные прямо перед ротацией, проходили проверку до конца своего срока жизни. Это перекрытие и делает ротацию плавной, а не аварией.
Короткий чек-лист по ключам:
- Ротируйте ключи подписи не реже раза в 90 дней и немедленно при любом подозрении на компрометацию.
- Публикуйте открытые ключи через JWKS; версионируйте их через
kid. - Держите закрытые ключи и секреты HMAC в KMS или HSM — никогда в git, никогда в клиентском коде, никогда захардкоженными.
- При утечке сразу ротируйте ключ и отзывайте действующие refresh-токены.
Проверка claims, которую нельзя пропускать
Проверка подписи доказывает, что токен подлинный. Но не доказывает, что токен предназначен вам и действует прямо сейчас. За это отвечает проверка claims, и это самая дешёвая защита, какую можно добавить. Пять claims нужно проверять на каждом запросе:
exp(expiration) — отклоняйте токены, срок которых уже истёк.nbf(not before) — отклоняйте токены, использованные до открытия их окна валидности.iat(issued at) — при желании отклоняйте токены, неправдоподобно старые.iss(issuer) — подтверждайте, что токен пришёл от издателя, которому вы доверяете.aud(audience) — подтверждайте, что токен выпущен для вашего сервиса. Отсутствующая проверкаaud— самая частая тихая дыра, позволяющая переиграть токен, выпущенный для одного API, против другого.
Большинство библиотек проверяют это за вас, когда вы передаёте ожидаемые значения:
jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5, // seconds, for distributed clock skew
});
Дайте небольшой допуск по часам, обычно пять секунд, чтобы незначительный сдвиг между серверами не отклонял в остальном валидные токены. Только не делайте его больше: щедрый допуск расширяет окно, в котором истёкший токен ещё работает, а ведь именно это окно exp и должен закрывать. Чтобы на глаз проверить значения exp и iat в токене, вставьте его в декодер JWT и переведите метки времени нашим конвертером Unix-времени.
Срок жизни токена и где хранить JWT
Проверки на стороне сервера — лишь половина дела. От того, где клиент хранит токен, зависит, насколько легко его украсть; здесь и сходятся XSS и угон сессии. Надёжный паттерн такой: короткоживущий access-токен (15–60 минут) в паре с отдельным, более долгоживущим refresh-токеном, который можно отозвать.
Выбор места хранения сводится к одному компромиссу:
| Место хранения | Уязвимость к XSS | Риск CSRF | Рекомендация |
|---|---|---|---|
| localStorage | Высокая — любой JavaScript на странице может его прочитать | Нет | Избегать для токенов сессии |
| Cookie с HttpOnly + Secure + SameSite=Strict | Низкая — невидим для JavaScript | Нужна защита от CSRF | Рекомендуется для сессий |
Токен в localStorage читает любой скрипт на странице, так что одна XSS-уязвимость сливает всю сессию, и атакующий переигрывает её со своей машины столько, сколько она живёт. Cookie с HttpOnly JavaScript не читает вовсе, и это сужает ущерб от XSS до того, что атакующий может сделать внутри живой страницы. Плохо, но это всё же не украденные учётные данные, которые уносят с собой. Платить за подход с cookie приходится защитой от CSRF, ведь cookie автоматически прицепляются к каждому запросу; SameSite=Strict плюс CSRF-токен это решают. Держите access-токен коротким, чтобы у утечки был малый радиус поражения, а refresh-токен кладите в cookie с HttpOnly, Secure, SameSite. При выходе или подозрении на компрометацию отзывайте refresh-токен на стороне сервера и ротируйте ключ подписи. О более широком контексте вокруг XSS, CSRF и безопасных cookie читайте в нашем руководстве по лучшим практикам веб-безопасности.
Чек-лист безопасности JWT
Пройдитесь по нему, прежде чем выкатывать любую аутентификацию на JWT:
- Проверка фиксирует явный список разрешённых алгоритмов и отклоняет
alg:none. - Асимметричная проверка жёстко задаёт ожидаемый алгоритм и никогда не читает
algиз заголовка (блокирует путаницу). - Секреты HS256 — минимум 32 случайных байта, загружаемые из KMS.
- Закрытые ключи живут в HSM/KMS; открытые ключи публикуются через JWKS и версионируются через
kid. - Ключи подписи ротируются не реже раза в 90 дней.
- Каждый запрос проверяет
exp,nbf,iat,issиaud, с допуском по часам в 5 секунд или меньше. - Access-токены живут 15–60 минут; refresh-токены лежат в cookie с
HttpOnly. - Никаких секретов в полезной нагрузке — она кодируется, а не шифруется.
FAQ
Безопасен ли JWT по умолчанию?
Нет. Безопасность JWT зависит от настройки. Нужно зафиксировать алгоритм, отклонять alg:none, использовать секрет или ключ с высокой энтропией и проверять claims. Настройки библиотек по умолчанию или снисходительные конфигурации часто допускают обход аутентификации.
Какая уязвимость JWT самая опасная?
Путаница алгоритмов, когда RS256 понижается до HS256, а открытый ключ используется как секрет HMAC. Она известна с 2015 года, но до сих пор всплывает в аудитах, потому что эксплуатирует серверы, выбирающие метод проверки по полю alg из заголовка.
Что выбрать — HS256 или RS256?
Берите HS256, когда одна и та же сторона подписывает и проверяет внутри одной границы доверия. Если же проверять должен другой сервис или третья сторона либо нужна ротация JWKS, выбирайте RS256 или ES256. Для новых систем лучше ES256: ключи меньше и быстрее при равной стойкости.
Где хранить JWT?
Для токенов сессии предпочитайте cookie с HttpOnly, Secure, SameSite, поскольку JavaScript не может его прочитать, а единственная XSS-уязвимость не сможет его украсть. Не держите токены сессии в localStorage: любой XSS сливает всю сессию, и её можно переиграть.
Как часто ротировать ключи подписи JWT?
Ротируйте не реже раза в 90 дней как рутину и немедленно при любом подозрении на компрометацию. Версионируйте ключи через kid и держите на проверяющей стороне и активный ключ, и недавний предыдущий, чтобы токены, подписанные прямо перед ротацией, по-прежнему проходили проверку.
Можно ли подделать JWT?
Без ключа подписи не подделает: ни один атакующий не соберёт токен, который пройдёт проверку. Но если ваш сервер принимает alg:none, уязвим к путанице алгоритмов или использует слабый секрет, подпись можно обойти. Это провалы в настройке, а не изъяны самого JWT.
Какие claims обязательно проверять?
Проверяйте exp (expiration), nbf (not before), iat (issued at), iss (issuer) и aud (audience). Отсутствующая проверка aud — самая частая тихая уязвимость, позволяющая переиграть токен, предназначенный для одного сервиса, против другого.
Заключение
Безопасность JWT не сложна, но устоять должен каждый слой. Подпись — ваша единственная гарантия, поэтому проверяйте её правильно. Зафиксируйте один явный алгоритм и никогда не доверяйте полю alg из заголовка. Берите стойкие, ротируемые ключи и храните их в KMS. Проверяйте exp, nbf, iat, iss и aud на каждом запросе. Держите токены там, куда не дотянется XSS.
Чтобы применить это на практике, вставьте токен в наш декодер JWT: изучите его алгоритм и claims, поймайте риски alg:none или путаницы HS. А с подписью поэкспериментируйте в энкодере JWT целиком в браузере, ваши ключи не покинут устройство.