Уязвимости, атаки, защита, best practices, безопасная реализация

JWT (JSON Web Tokens): уязвимости, атаки, защита и best practices безопасной реализации. Структура токенов, алгоритмы подписи, безопасное использование.

JWT - JSON Web Tokens

Определение

JWT (JSON Web Token) — это открытый стандарт (RFC 7519) для безопасной передачи информации между сторонами в виде компактного JSON-объекта. JWT используется для аутентификации и авторизации в веб-приложениях, особенно в REST API и микросервисной архитектуре.

Структура JWT

JWT состоит из трех частей, разделенных точками (.):

header.payload.signature

1. Header (Заголовок)

{
  "alg": "HS256",
  "typ": "JWT"
}

Поля заголовка:

  • alg — алгоритм подписи (HS256, RS256, ES256)
  • typ — тип токена (обычно “JWT”)
  • kid — идентификатор ключа (опционально)

2. Payload (Полезная нагрузка)

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "iss": "example.com",
  "aud": "api.example.com"
}

Стандартные claims (заявки):

  • iss (issuer) — издатель токена
  • sub (subject) — субъект токена
  • aud (audience) — аудитория токена
  • exp (expiration) — время истечения
  • nbf (not before) — не раньше
  • iat (issued at) — время выдачи
  • jti (JWT ID) — уникальный идентификатор

3. Signature (Подпись)

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

Алгоритмы подписи

1. HMAC (Симметричные алгоритмы)

  • HS256 — HMAC с SHA-256
  • HS384 — HMAC с SHA-384
  • HS512 — HMAC с SHA-512
// Пример создания JWT с HS256
const jwt = require("jsonwebtoken");

const token = jwt.sign({ userId: 123, role: "admin" }, "secret-key", {
  expiresIn: "1h",
});

2. RSA (Асимметричные алгоритмы)

  • RS256 — RSA с SHA-256
  • RS384 — RSA с SHA-384
  • RS512 — RSA с SHA-512
// Пример с RS256
const token = jwt.sign(payload, privateKey, {
  algorithm: "RS256",
  expiresIn: "1h",
});

// Верификация с публичным ключом
const decoded = jwt.verify(token, publicKey, { algorithms: ["RS256"] });

3. ECDSA (Эллиптические кривые)

  • ES256 — ECDSA с P-256 и SHA-256
  • ES384 — ECDSA с P-384 и SHA-384
  • ES512 — ECDSA с P-521 и SHA-512

Типы JWT

1. Signed JWT (JWS)

  • Содержит цифровую подпись
  • Обеспечивает целостность и аутентичность
  • Наиболее распространенный тип

2. Encrypted JWT (JWE)

  • Содержит зашифрованную полезную нагрузку
  • Обеспечивает конфиденциальность данных
  • Используется для чувствительной информации

3. Unsecured JWT

  • Без подписи и шифрования
  • Используется только для передачи данных
  • Не рекомендуется для аутентификации

Применение JWT

1. Аутентификация

// Логин пользователя
app.post("/login", async (req, res) => {
  const { username, password } = req.body;

  // Проверка учетных данных
  const user = await authenticateUser(username, password);

  if (user) {
    const token = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: "24h" }
    );

    res.json({ token });
  } else {
    res.status(401).json({ error: "Invalid credentials" });
  }
});

2. Авторизация

// Middleware для проверки JWT
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ error: "Access token required" });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: "Invalid token" });
    }
    req.user = user;
    next();
  });
};

3. API Gateway

// Проверка токена в API Gateway
const validateToken = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(" ")[1];

    if (!token) {
      return res.status(401).json({ error: "No token provided" });
    }

    const decoded = jwt.verify(token, publicKey);

    // Проверка времени истечения
    if (decoded.exp < Date.now() / 1000) {
      return res.status(401).json({ error: "Token expired" });
    }

    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).json({ error: "Invalid token" });
  }
};

Уязвимости JWT

1. Алгоритм “none”

{
  "alg": "none",
  "typ": "JWT"
}

Проблема: Некоторые библиотеки поддерживают алгоритм “none”, что позволяет создавать токены без подписи.

Защита:

// Явно указывать разрешенные алгоритмы
jwt.verify(token, secret, { algorithms: ["HS256", "RS256"] });

2. Слабая секретная строка

// НЕПРАВИЛЬНО - слабый секрет
const token = jwt.sign(payload, "secret");

// ПРАВИЛЬНО - криптографически стойкий секрет
const token = jwt.sign(payload, crypto.randomBytes(64).toString("hex"));

3. Раскрытие секретного ключа

  • Хранение секрета в коде приложения
  • Передача секрета через небезопасные каналы
  • Использование одного секрета для всех окружений

4. Отсутствие проверки алгоритма

// УЯЗВИМО - принимает любой алгоритм
jwt.verify(token, secret);

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

5. Timing атаки

// УЯЗВИМО - разное время выполнения для разных токенов
if (token === expectedToken) {
  return true;
}

// БЕЗОПАСНО - константное время сравнения
if (crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expectedToken))) {
  return true;
}

Атаки на JWT

1. Brute Force атаки

# Использование hashcat для подбора секрета
hashcat -m 16500 jwt.txt wordlist.txt

# Использование john the ripper
john --wordlist=wordlist.txt jwt.txt

2. Key Confusion атаки

// Атака: использование публичного ключа как секрета
const maliciousToken = jwt.sign(
  { admin: true },
  publicKey, // Используем публичный ключ
  { algorithm: "HS256" }
);

3. Header Injection

{
  "alg": "HS256",
  "typ": "JWT",
  "kid": "../../../etc/passwd"
}

4. Algorithm Substitution

// Изменение алгоритма с RS256 на HS256
{
  "alg": "HS256", // Было RS256
  "typ": "JWT"
}

Безопасные практики

1. Выбор алгоритма

// Рекомендуется RS256 для распределенных систем
const token = jwt.sign(payload, privateKey, {
  algorithm: "RS256",
  expiresIn: "15m", // Короткое время жизни
});

2. Управление ключами

// Ротация ключей
const keyId = "key-" + Date.now();
const token = jwt.sign(payload, privateKey, {
  algorithm: "RS256",
  keyid: keyId,
  expiresIn: "1h",
});

3. Валидация токенов

const validateJWT = (token) => {
  try {
    // Проверка структуры
    const parts = token.split(".");
    if (parts.length !== 3) {
      throw new Error("Invalid token structure");
    }

    // Проверка заголовка
    const header = JSON.parse(base64UrlDecode(parts[0]));
    if (!header.alg || !header.typ) {
      throw new Error("Invalid header");
    }

    // Проверка полезной нагрузки
    const payload = JSON.parse(base64UrlDecode(parts[1]));
    if (!payload.exp || payload.exp < Date.now() / 1000) {
      throw new Error("Token expired");
    }

    // Проверка подписи
    jwt.verify(token, publicKey, { algorithms: ["RS256"] });

    return payload;
  } catch (error) {
    throw new Error("Token validation failed");
  }
};

4. Хранение токенов

// Безопасное хранение в HTTP-only cookies
res.cookie("token", token, {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
  maxAge: 15 * 60 * 1000, // 15 минут
});

// НЕ хранить в localStorage для чувствительных данных
// localStorage.setItem('token', token); // НЕБЕЗОПАСНО

Инструменты для тестирования JWT

1. jwt.io

  • Онлайн-декодер и валидатор JWT
  • Поддержка различных алгоритмов
  • Возможность подписи токенов

2. jwt_tool

# Установка
npm install -g jwt_tool

# Анализ токена
jwt_tool <JWT_TOKEN>

# Атака на алгоритм
jwt_tool <JWT_TOKEN> -T

# Подбор секрета
jwt_tool <JWT_TOKEN> -C -d wordlist.txt

3. Burp Suite Extension

  • JWT Editor для модификации токенов
  • Автоматическое тестирование уязвимостей
  • Интеграция с Burp Scanner

Современные альтернативы

1. PASETO (Platform-Agnostic Security Tokens)

  • Более безопасная альтернатива JWT
  • Предотвращает многие уязвимости JWT
  • Простая реализация

2. OAuth 2.0 + OpenID Connect

  • Стандартизированный подход к аутентификации
  • Встроенные механизмы безопасности
  • Широкая поддержка провайдеров

3. Session-based аутентификация

  • Традиционный подход с серверными сессиями
  • Более безопасен для некоторых сценариев
  • Проще в реализации

Практические примеры

Создание безопасного JWT сервиса:

const express = require("express");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");

const app = express();

// Генерация ключей
const privateKey = crypto.generateKeyPairSync("rsa", {
  modulusLength: 2048,
  publicKeyEncoding: { type: "spki", format: "pem" },
  privateKeyEncoding: { type: "pkcs8", format: "pem" },
});

// Создание токена
const createToken = (payload) => {
  return jwt.sign(payload, privateKey.privateKey, {
    algorithm: "RS256",
    expiresIn: "15m",
    issuer: "example.com",
    audience: "api.example.com",
  });
};

// Верификация токена
const verifyToken = (token) => {
  return jwt.verify(token, privateKey.publicKey, {
    algorithms: ["RS256"],
    issuer: "example.com",
    audience: "api.example.com",
  });
};

// Middleware аутентификации
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ error: "Access token required" });
  }

  try {
    const decoded = verifyToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).json({ error: "Invalid or expired token" });
  }
};

app.post("/login", (req, res) => {
  const { username, password } = req.body;

  // Проверка учетных данных
  if (authenticateUser(username, password)) {
    const token = createToken({
      sub: username,
      role: getUserRole(username),
    });

    res.json({
      access_token: token,
      token_type: "Bearer",
      expires_in: 900, // 15 минут
    });
  } else {
    res.status(401).json({ error: "Invalid credentials" });
  }
});

app.get("/protected", authenticateToken, (req, res) => {
  res.json({
    message: "Protected resource",
    user: req.user.sub,
  });
});

Заключение

JWT является мощным инструментом для аутентификации и авторизации в современных веб-приложениях, но требует тщательного подхода к безопасности:

  • Правильный выбор алгоритмов подписи
  • Безопасное управление ключами
  • Короткое время жизни токенов
  • Строгая валидация всех компонентов
  • Регулярное тестирование на уязвимости

Понимание принципов работы JWT и связанных с ними рисков критически важно для разработчиков и специалистов по безопасности, работающих с современными API и микросервисными архитектурами.