JWT — семь строк, которые обходят авторизацию
На первый взгляд код выглядит аккуратно — импорт библиотеки, секрет из переменной окружения, одна функция проверки:
import jwt from "jsonwebtoken";
const KEY = process.env.KEY;
export function verifyToken(token) {
return jwt.verify(token, KEY);
}
Семь строк, типичный фрагмент middleware для Express или Fastify. Код компилируется, проходит ревью («секрет в env, verify вместо decode — молодцы») и уезжает в прод.
Проблема в том, что jwt.verify(token, KEY) доверяет заголовку токена alg, если вы явно не перечислили допустимые алгоритмы. При удачной эксплуатации атакующий подставляет свой payload (sub, role, admin: true) и проходит проверку без знания секрета.
Структура JWT, cookie/session и OAuth — в аутентификации и авторизации. Здесь разбираем типичные ошибки проверки на Node.js с пакетом jsonwebtoken.
Почему «просто verify» недостаточно
JWT состоит из header.payload.signature. Поле alg в header говорит серверу, как проверять подпись — HMAC (HS256), RSA (RS256), ECDSA и т.д.
Без опции { algorithms: [...] } библиотека берёт алгоритм из токена и подбирает способ проверки под переданный ключ. Это открывает два класса атак.
| Ошибка в коде | Что делает атакующий | Итог |
|---|---|---|
Нет whitelist algorithms | Подменяет alg и пересчитывает подпись | Любой sub / роль в payload |
KEY — RSA публичный ключ, алгоритм не зафиксирован | Меняет RS256 → HS256, подписывает HMAC тем же PEM-текстом | Подделка без приватного ключа |
const KEY = process.env.KEY при загрузке модуля | Ждёт, пока KEY === undefined (dotenv позже, тест без env) | Непредсказуемое поведение, «работает локально» |
| Секрет не проверяют при старте | Пустой или дефолтный ключ в .env.example | Brute-force или угадывание |
Успешная эксплуатация даёт полный обход аутентификации: атакующий выдаёт себя за любого пользователя, подставляя нужный sub и claims в payload.
Атака с alg: none
none — алгоритм «без подписи». Токен из двух сегментов и пустой третьей части:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.
Расшифровка header:
{"alg": "none", "typ": "JWT"}
Payload — любой JSON с нужным sub, role, exp.
В старых версиях jsonwebtoken (до жёстких дефолтов ~2015–2018) такой токен проходил verify, если разработчик не указал algorithms. Библиотека видела alg: none и не требовала подписи.
Современные версии (v9+) по умолчанию отклоняют none, пока вы сами не добавите его в whitelist — сообщение вроде please specify "none" in "algorithms". Это не повод расслабляться: достаточно скопировать из Stack Overflow { algorithms: ['HS256', 'none'] } «на всякий случай», и дыра вернётся.
Защита — явный список только тех алгоритмов, которые реально использует ваш IdP:
jwt.verify(token, KEY, { algorithms: ["HS256"] });
Подмена алгоритма RS256 → HS256
Классическая algorithm confusion (CVE-2015-9235, затронуты многие JWT-библиотеки).
Сценарий:
- Сервер подписывает access-токены RSA (приватный ключ только у auth-сервиса).
- Resource server для проверки хранит публичный PEM в
process.env.KEY— это нормальная схема. - Код снова
jwt.verify(token, KEY)безalgorithms.
Атакующий:
- Берёт публичный ключ (он часто лежит в
/.well-known/jwks.json, в репозитории, в Docker-образе). - Собирает header с
"alg": "HS256". - Пишет payload с
"sub": "1","role": "admin". - Считает HMAC-SHA256 от
base64url(header).base64url(payload), используя строку публичного PEM как симметричный секрет. - Отправляет подделанный токен в
Authorization: Bearer ….
Сервер видит HS256, переданный «секрет» совпадает с PEM — проверка проходит. Приватный ключ атакующему не нужен.
Для HS256 передавайте общий секрет и { algorithms: ["HS256"] }. Для RS256 — только публичный ключ (или JWKS) и { algorithms: ["RS256"] }. Никогда не смешивайте «один KEY на всё» без явного whitelist.
Секрет на уровне модуля и пустой KEY
Вторая ловушка в тех же семи строках — const KEY = process.env.KEY при импорте файла.
// app.js
import dotenv from "dotenv";
import { verifyToken } from "./auth.js"; // KEY уже прочитан — undefined
dotenv.config(); // env появился слишком поздно
Node кэширует модули: KEY остаётся undefined на всё время процесса. Локально «всё ок», если env задан в shell до node, а в Docker/K8s без переменной — тихий сбой или странные ошибки verify.
Типичные симптомы:
- в prod «401 на всех запросах», пока кто-то не добавит env;
- в dev токены «магически» проходят из-за другого порядка загрузки;
- тесты мокают env после первого import — проверка идёт с чужим ключом.
Правило — читать секрет внутри функции или после гарантированной инициализации конфига; при старте приложения падать, если секрет пуст:
function getKey() {
const key = process.env.JWT_SECRET;
if (!key) {
throw new Error("JWT_SECRET is not configured");
}
return key;
}
Безопасная проверка — эталон
Минимально приемлемый вариант для симметричного HS256:
import jwt from "jsonwebtoken";
export function verifyToken(token) {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("JWT_SECRET is not configured");
}
return jwt.verify(token, secret, {
algorithms: ["HS256"],
issuer: "https://auth.example.com",
audience: "my-api",
clockTolerance: 5,
});
}
Для RS256 через JWKS (Keycloak, Auth0, Cognito) — algorithms: ["RS256"], ключ из JWKS endpoint, те же проверки iss / aud / exp.
| Проверка | Зачем |
|---|---|
algorithms: [...] | Запрет none и подмены RS256↔HS256 |
issuer / audience | Токен выдан вашим IdP и для вашего API |
exp, nbf | Отсечь просроченные и «из будущего» |
| Секрет при старте | Fail fast вместо «verify с undefined» |
jwt.verify, не jwt.decode | Decode не проверяет подпись |
Spring oauth2ResourceServer, Nest @nestjs/jwt с явным algorithms, Fastify @fastify/jwt — всё это задаёт whitelist за вас, если конфиг не ослаблен вручную. Сырой jsonwebtoken в middleware — зона повышенного риска.
Чек-лист перед продом
- В
jwt.verifyуказан единственный ожидаемый алгоритм (HS256илиRS256, не оба «на всякий случай»). - В whitelist нет
none. - Секрет / JWKS URI проверяются при старте приложения.
- Env загружается до первого import модуля с verify (или ключ читается лениво).
- Проверяются
iss,aud,exp(и при необходимостиsubпротив своей БД). - Нигде в коде нет
jwt.decodeтам, где нужна авторизация. - Публичный RSA-ключ не используется как HMAC-секрет — разные конфиги для sign и verify.
Связанные материалы
| Тема | Статья |
|---|---|
| JWT, session, PASETO, OAuth | Аутентификация и авторизация |
| Broken Access Control, IDOR | 8.07 / 128 — каталог угроз |
| Rate limit на API | Rate limiting |
| Триада CIA | Основы информационной безопасности |
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Основы информационной безопасности - роль политик, контроль доступа и базовые принципы защиты данных в корпоративных системах. Аутентификация и авторизация - уровни проверки личности, двухфакторная защита и разграничение прав доступа в информационных системах. Антивирусная защита и лечение заражений - как распознать компрометацию системы, локализовать угрозу и восстановить рабочее состояние. Риски открытых Wi-Fi сетей - как работают passive/active-сканирование и почему публичные точки доступа требуют дополнительных мер защиты. Надёжность паролей — хеширование, соль, менеджеры паролей и типы атак: перебор, словарь, credential stuffing, фишинг, MITM. Фаерволы и сетевые пакеты - как межсетевой экран анализирует трафик и применяет правила фильтрации для защиты инфраструктуры. SSH — это протокол безопасного удалённого доступа к компьютерным системам. DoS и DDoS — как перегружают сервер, ботнет и SYN-flood, уровни атаки L3–L7 и базовая защита. Эндпоинт change-password без проверки текущего пароля — захват аккаунта при украденной сессии, CSRF и чек-лист безопасной смены пароля. Broken Access Control — проверка прав через query-параметр, cookie или заголовок с клиента; эксплуатация за секунду и серверная авторизация. Краткие итоги раздела "Основы информационной безопасности". Чек-лист раздела Основы информационной безопасности — вопросы для самопроверки в энциклопедии Вселенная IT.Основы информационной безопасности
Аутентификация и авторизация
Антивирусная защита и лечение заражённых систем
Риски открытых Wi-Fi сетей
Устройство и надёжность паролей
Фаерволы
Шифрование данных и протокол SSH
DDoS и отказ в обслуживании
Смена пароля — пропущенный шаг re-auth
Админка по ?isAdmin=true
Итоги
Чек-лист самопроверки