lenec ru

← все посты

Zustand vs Redux Toolkit в 2026: какой стейт-менеджер выбрать для React

16K

JWT стал стандартом де-факто для аутентификации в SPA и микросервисах. Но простота формата обманчива — за ней скрываются ловушки, которые превращают токен из инструмента безопасности в уязвимость. Вот семь ошибок, которые встречаются в продакшене снова и снова.

1. Хранение токена в localStorage

Любой XSS на странице получает полный доступ к токену из localStorage:

// Плохо: любой инжектированный скрипт украдёт токен
localStorage.setItem('token', jwt);

// Хорошо: httpOnly cookie недоступна из JS
res.cookie('access_token', jwt, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 15 * 60 * 1000
});

httpOnly cookie не панацея — нужна защита от CSRF. Но она закрывает весь класс XSS-атак на кражу сессии.

2. Отсутствие валидации алгоритма (alg:none)

Атака «alg:none» — злоумышленник подменяет заголовок на {"alg": "none"} и отправляет токен без подписи:

// УЯЗВИМО: библиотека выбирает алгоритм из заголовка
const payload = jwt.verify(token, secret);

// БЕЗОПАСНО: явно указываем допустимые алгоритмы
const payload = jwt.verify(token, secret, {
  algorithms: ['HS256']
});

Ещё опаснее — путаница RS256/HS256. Атакующий подписывает токен публичным ключом с alg: HS256, и сервер принимает подделку.

3. Слишком долгий TTL без refresh-ротации

Access token с TTL в 24 часа — окно, в течение которого украденный токен даёт полный доступ. Правильная схема:

// Access — 15 минут, refresh — одноразовый
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });
const refreshToken = crypto.randomUUID();
await redis.set(`refresh:${refreshToken}`, userId, 'EX', 7 * 86400);

// Ротация: старый refresh удаляется, выдаётся новый
async function rotateRefresh(oldToken: string) {
  const userId = await redis.get(`refresh:${oldToken}`);
  if (!userId) throw new UnauthorizedError('Token revoked');
  await redis.del(`refresh:${oldToken}`);
  const newRefresh = crypto.randomUUID();
  await redis.set(`refresh:${newRefresh}`, userId, 'EX', 7 * 86400);
  return { accessToken: signAccess(userId), refreshToken: newRefresh };
}

Если refresh token используется дважды — сигнал компрометации. Инвалидируйте всю цепочку.

4. Секрет в коде и слабый symmetric key

// Плохо: короткий, словарный
const secret = 'my-jwt-secret';

// Хорошо: минимум 256 бит энтропии для HS256
// Генерация: openssl rand -base64 32
const secret = process.env.JWT_SECRET; // 64 символа base64

Для продакшена рассмотрите RS256 с ротацией ключей через JWKS endpoint — позволяет менять ключи без инвалидации всех токенов.

5. Отсутствие revocation — logout не работает

JWT stateless — сервер не хранит состояние. «Выйти из системы» клиент не может: токен валиден до истечения TTL.

// Blacklist в Redis
async function logout(token: string) {
  const decoded = jwt.decode(token) as JwtPayload;
  const ttl = decoded.exp! - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await redis.set(`blacklist:${token}`, '1', 'EX', ttl);
  }
}

// В middleware:
if (await redis.exists(`blacklist:${token}`)) {
  return res.status(401).json({ error: 'Token revoked' });
}

Альтернатива: версионирование — tokenVersion в БД пользователя, при logout инкрементируется.

6. Раздутый payload

JWT в каждом запросе. Если payload содержит роли, пермишены, профиль — токен раздувается. HTTP-заголовки ограничены (8KB nginx), cookie — 4KB на домен.

// Плохо: всё в токене
const payload = { sub: userId, roles: [...], permissions: [...50 items], profile: {...} };

// Хорошо: минимум в токене
const payload = { sub: userId, role: 'admin' };
// Детальные пермишены — запрос к сервису или кэш

7. Игнорирование aud/iss claims

Без проверки aud и iss токен от сервиса A принимается сервисом B:

// Создание с указанием audience
const token = jwt.sign(payload, secret, {
  issuer: 'auth.myapp.com',
  audience: 'api.myapp.com',
  expiresIn: '15m'
});

// Проверка — требуем совпадения
const payload = jwt.verify(token, secret, {
  issuer: 'auth.myapp.com',
  audience: 'api.myapp.com',
  algorithms: ['HS256']
});

В микросервисах это критично — без aud токен публичного API может открыть доступ к admin-сервису.

Чеклист

  • Токен в httpOnly secure cookie, не в localStorage
  • Алгоритм зафиксирован, algorithms передан явно
  • Access TTL 5-15 минут, refresh с ротацией
  • Секрет минимум 256 бит, в vault/env, не в коде
  • Есть механизм revocation
  • Payload минимальный: sub, role, iat, exp, aud, iss
  • aud и iss проверяются при верификации

Каждая из этих ошибок — реальный вектор атаки. Исправление занимает минуты, а последствия эксплуатации — недели разбора инцидента.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.