Структура токенов, алгоритмы подписи, уязвимости JWT

JWT Security: структура токенов, алгоритмы подписи, распространенные уязвимости JWT. Атаки на JWT, методы защиты и безопасная реализация.

JWT Security - Распространенные уязвимости

Что такое JWT?

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

Структура JWT

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

header.payload.signature

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

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

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

Signature (Подпись)

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

Распространенные уязвимости JWT

1. Algorithm Confusion Attack

Атака на путаницу алгоритмов

Описание

Атака, при которой злоумышленник изменяет алгоритм в заголовке JWT с HMAC на RSA, используя публичный ключ для подписи токена.

Примеры реализации

// Уязвимая реализация
class VulnerableJWT {
  // ❌ Неправильно: отсутствие проверки алгоритма
  verifyToken(token, secret) {
    const [header, payload, signature] = token.split(".");

    // Декодирование заголовка
    const headerObj = JSON.parse(atob(header));

    // Создание подписи
    const expectedSignature = this.createSignature(
      header,
      payload,
      secret,
      headerObj.alg
    );

    // Сравнение подписей
    return signature === expectedSignature;
  }

  // Создание подписи
  createSignature(header, payload, secret, algorithm) {
    switch (algorithm) {
      case "HS256":
        return this.hmacSha256(header + "." + payload, secret);
      case "RS256":
        return this.rsaSha256(header + "." + payload, secret);
      default:
        throw new Error("Unsupported algorithm");
    }
  }
}

// Безопасная реализация
class SecureJWT {
  constructor() {
    this.allowedAlgorithms = ["HS256", "HS384", "HS512"];
    this.publicKeys = new Map();
  }

  // ✅ Правильно: проверка алгоритма
  verifyToken(token, secret) {
    try {
      const [header, payload, signature] = token.split(".");

      // Декодирование заголовка
      const headerObj = JSON.parse(atob(header));

      // Проверка алгоритма
      if (!this.allowedAlgorithms.includes(headerObj.alg)) {
        throw new Error("Algorithm not allowed");
      }

      // Проверка типа
      if (headerObj.typ !== "JWT") {
        throw new Error("Invalid token type");
      }

      // Создание подписи
      const expectedSignature = this.createSignature(
        header,
        payload,
        secret,
        headerObj.alg
      );

      // Безопасное сравнение подписей
      if (!this.secureCompare(signature, expectedSignature)) {
        throw new Error("Invalid signature");
      }

      // Декодирование payload
      const payloadObj = JSON.parse(atob(payload));

      // Проверка времени жизни
      if (payloadObj.exp && payloadObj.exp < Date.now() / 1000) {
        throw new Error("Token expired");
      }

      return payloadObj;
    } catch (error) {
      console.error("Token verification failed:", error);
      throw new Error("Invalid token");
    }
  }

  // Безопасное сравнение строк
  secureCompare(a, b) {
    if (a.length !== b.length) {
      return false;
    }

    let result = 0;
    for (let i = 0; i < a.length; i++) {
      result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }

    return result === 0;
  }

  // Создание подписи
  createSignature(header, payload, secret, algorithm) {
    switch (algorithm) {
      case "HS256":
        return this.hmacSha256(header + "." + payload, secret);
      case "HS384":
        return this.hmacSha384(header + "." + payload, secret);
      case "HS512":
        return this.hmacSha512(header + "." + payload, secret);
      default:
        throw new Error("Unsupported algorithm");
    }
  }
}

2. None Algorithm Attack

Атака с алгоритмом None

Описание

Атака, при которой злоумышленник изменяет алгоритм в заголовке на “none”, что позволяет обойти проверку подписи.

Примеры реализации

// Уязвимая реализация
class VulnerableNoneJWT {
  // ❌ Неправильно: поддержка алгоритма none
  verifyToken(token, secret) {
    const [header, payload, signature] = token.split(".");
    const headerObj = JSON.parse(atob(header));

    if (headerObj.alg === "none") {
      // Опасная логика - пропуск проверки подписи
      return JSON.parse(atob(payload));
    }

    // Обычная проверка подписи
    return this.verifyWithSignature(token, secret);
  }
}

// Безопасная реализация
class SecureNoneJWT {
  constructor() {
    this.allowedAlgorithms = ["HS256", "HS384", "HS512"];
    this.blockedAlgorithms = ["none", "None", "NONE"];
  }

  // ✅ Правильно: блокировка алгоритма none
  verifyToken(token, secret) {
    const [header, payload, signature] = token.split(".");
    const headerObj = JSON.parse(atob(header));

    // Проверка на заблокированные алгоритмы
    if (this.blockedAlgorithms.includes(headerObj.alg)) {
      throw new Error("Algorithm not allowed");
    }

    // Проверка на разрешенные алгоритмы
    if (!this.allowedAlgorithms.includes(headerObj.alg)) {
      throw new Error("Algorithm not supported");
    }

    // Обычная проверка подписи
    return this.verifyWithSignature(token, secret);
  }
}

3. Weak Secret Attack

Атака на слабый секрет

Описание

Атака, при которой злоумышленник пытается угадать или взломать секретный ключ, используемый для подписи JWT.

Примеры реализации

// Уязвимая реализация
class VulnerableSecretJWT {
  // ❌ Неправильно: слабый секрет
  generateSecret() {
    return "secret123"; // Слишком простой секрет
  }

  // ❌ Неправильно: захардкоженный секрет
  verifyToken(token) {
    const secret = "mySecretKey"; // Захардкоженный секрет
    return this.verifyWithSecret(token, secret);
  }
}

// Безопасная реализация
class SecureSecretJWT {
  constructor() {
    this.secretManager = new SecretManager();
  }

  // ✅ Правильно: генерация сильного секрета
  async generateSecret() {
    const crypto = require("crypto");
    return crypto.randomBytes(64).toString("hex");
  }

  // ✅ Правильно: безопасное хранение секрета
  async storeSecret(secret) {
    // Шифрование секрета
    const encryptedSecret = await this.encryptSecret(secret);

    // Безопасное хранение
    await this.secretManager.store("jwt_secret", encryptedSecret);
  }

  // ✅ Правильно: безопасное получение секрета
  async getSecret() {
    const encryptedSecret = await this.secretManager.get("jwt_secret");
    return await this.decryptSecret(encryptedSecret);
  }

  // ✅ Правильно: ротация секрета
  async rotateSecret() {
    const newSecret = await this.generateSecret();
    await this.storeSecret(newSecret);

    // Уведомление о ротации
    await this.notifySecretRotation();
  }

  // Шифрование секрета
  async encryptSecret(secret) {
    const crypto = require("crypto");
    const key = await this.getMasterKey();
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher("aes-256-gcm", key);

    let encrypted = cipher.update(secret, "utf8", "hex");
    encrypted += cipher.final("hex");

    return {
      encrypted: encrypted,
      iv: iv.toString("hex"),
      authTag: cipher.getAuthTag().toString("hex"),
    };
  }

  // Расшифровка секрета
  async decryptSecret(encryptedSecret) {
    const crypto = require("crypto");
    const key = await this.getMasterKey();
    const decipher = crypto.createDecipher("aes-256-gcm", key);

    decipher.setAuthTag(Buffer.from(encryptedSecret.authTag, "hex"));

    let decrypted = decipher.update(encryptedSecret.encrypted, "hex", "utf8");
    decrypted += decipher.final("utf8");

    return decrypted;
  }
}

4. Timing Attack

Атака по времени

Описание

Атака, при которой злоумышленник использует различия во времени выполнения для получения информации о секретном ключе.

Примеры реализации

// Уязвимая реализация
class VulnerableTimingJWT {
  // ❌ Неправильно: уязвимое сравнение
  verifyToken(token, secret) {
    const [header, payload, signature] = token.split(".");
    const expectedSignature = this.createSignature(header, payload, secret);

    // Опасное сравнение - уязвимо к timing attack
    return signature === expectedSignature;
  }
}

// Безопасная реализация
class SecureTimingJWT {
  // ✅ Правильно: безопасное сравнение
  verifyToken(token, secret) {
    const [header, payload, signature] = token.split(".");
    const expectedSignature = this.createSignature(header, payload, secret);

    // Безопасное сравнение - защищено от timing attack
    return this.secureCompare(signature, expectedSignature);
  }

  // Безопасное сравнение строк
  secureCompare(a, b) {
    if (a.length !== b.length) {
      return false;
    }

    let result = 0;
    for (let i = 0; i < a.length; i++) {
      result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }

    return result === 0;
  }
}

5. JWT Header Injection

Инъекция в заголовок JWT

Описание

Атака, при которой злоумышленник внедряет вредоносные данные в заголовок JWT для обхода проверок безопасности.

Примеры реализации

// Уязвимая реализация
class VulnerableHeaderJWT {
  // ❌ Неправильно: отсутствие валидации заголовка
  createToken(payload, secret) {
    const header = {
      alg: "HS256",
      typ: "JWT",
    };

    const encodedHeader = btoa(JSON.stringify(header));
    const encodedPayload = btoa(JSON.stringify(payload));
    const signature = this.createSignature(
      encodedHeader,
      encodedPayload,
      secret
    );

    return `${encodedHeader}.${encodedPayload}.${signature}`;
  }
}

// Безопасная реализация
class SecureHeaderJWT {
  constructor() {
    this.allowedAlgorithms = ["HS256", "HS384", "HS512"];
    this.allowedTypes = ["JWT"];
  }

  // ✅ Правильно: валидация заголовка
  createToken(payload, secret) {
    const header = this.validateHeader({
      alg: "HS256",
      typ: "JWT",
    });

    const encodedHeader = btoa(JSON.stringify(header));
    const encodedPayload = btoa(JSON.stringify(payload));
    const signature = this.createSignature(
      encodedHeader,
      encodedPayload,
      secret
    );

    return `${encodedHeader}.${encodedPayload}.${signature}`;
  }

  // Валидация заголовка
  validateHeader(header) {
    // Проверка алгоритма
    if (!this.allowedAlgorithms.includes(header.alg)) {
      throw new Error("Algorithm not allowed");
    }

    // Проверка типа
    if (!this.allowedTypes.includes(header.typ)) {
      throw new Error("Type not allowed");
    }

    // Проверка на инъекцию
    if (this.containsInjection(header)) {
      throw new Error("Header contains malicious content");
    }

    return header;
  }

  // Проверка на инъекцию
  containsInjection(header) {
    const headerStr = JSON.stringify(header);

    // Проверка на XSS
    if (/<script/i.test(headerStr)) {
      return true;
    }

    // Проверка на SQL инъекцию
    if (/union\s+select/i.test(headerStr)) {
      return true;
    }

    // Проверка на команды
    if (/[;&|`$]/i.test(headerStr)) {
      return true;
    }

    return false;
  }
}

Best Practices для JWT Security

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

  • Используйте HMAC для симметричного шифрования
  • Используйте RSA для асимметричного шифрования
  • Избегайте алгоритма none
  • Регулярно обновляйте алгоритмы

2. Управление секретами

  • Генерируйте сильные секреты (минимум 256 бит)
  • Храните секреты безопасно (HSM, Key Vault)
  • Регулярно ротируйте секреты
  • Не захардкоживайте секреты в коде

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

  • Проверяйте алгоритм в заголовке
  • Проверяйте время жизни токена
  • Проверяйте подпись токена
  • Валидируйте payload токена

4. Безопасное хранение

  • Не храните JWT в localStorage для чувствительных данных
  • Используйте httpOnly cookies для веб-приложений
  • Шифруйте JWT при хранении
  • Ограничивайте время жизни токенов

Примеры безопасной реализации

1. Полная реализация JWT

// Безопасная реализация JWT
class SecureJWTImplementation {
  constructor() {
    this.crypto = require("crypto");
    this.allowedAlgorithms = ["HS256", "HS384", "HS512"];
    this.defaultAlgorithm = "HS256";
    this.defaultExpiration = 3600; // 1 час
  }

  // Создание JWT
  async createToken(payload, secret, options = {}) {
    try {
      // Валидация payload
      this.validatePayload(payload);

      // Создание заголовка
      const header = {
        alg: options.algorithm || this.defaultAlgorithm,
        typ: "JWT",
      };

      // Валидация заголовка
      this.validateHeader(header);

      // Добавление стандартных claims
      const now = Math.floor(Date.now() / 1000);
      const tokenPayload = {
        ...payload,
        iat: now,
        exp: now + (options.expiration || this.defaultExpiration),
        jti: this.generateJTI(),
      };

      // Кодирование
      const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
      const encodedPayload = this.base64UrlEncode(JSON.stringify(tokenPayload));

      // Создание подписи
      const signature = await this.createSignature(
        encodedHeader + "." + encodedPayload,
        secret,
        header.alg
      );

      return `${encodedHeader}.${encodedPayload}.${signature}`;
    } catch (error) {
      console.error("Error creating token:", error);
      throw new Error("Token creation failed");
    }
  }

  // Верификация JWT
  async verifyToken(token, secret) {
    try {
      // Разбор токена
      const parts = token.split(".");
      if (parts.length !== 3) {
        throw new Error("Invalid token format");
      }

      const [encodedHeader, encodedPayload, signature] = parts;

      // Декодирование заголовка
      const header = JSON.parse(this.base64UrlDecode(encodedHeader));

      // Валидация заголовка
      this.validateHeader(header);

      // Декодирование payload
      const payload = JSON.parse(this.base64UrlDecode(encodedPayload));

      // Валидация payload
      this.validatePayload(payload);

      // Проверка времени жизни
      if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
        throw new Error("Token expired");
      }

      // Проверка подписи
      const expectedSignature = await this.createSignature(
        encodedHeader + "." + encodedPayload,
        secret,
        header.alg
      );

      if (!this.secureCompare(signature, expectedSignature)) {
        throw new Error("Invalid signature");
      }

      return payload;
    } catch (error) {
      console.error("Error verifying token:", error);
      throw new Error("Token verification failed");
    }
  }

  // Создание подписи
  async createSignature(data, secret, algorithm) {
    switch (algorithm) {
      case "HS256":
        return this.hmacSha256(data, secret);
      case "HS384":
        return this.hmacSha384(data, secret);
      case "HS512":
        return this.hmacSha512(data, secret);
      default:
        throw new Error("Unsupported algorithm");
    }
  }

  // HMAC SHA256
  hmacSha256(data, secret) {
    return this.crypto
      .createHmac("sha256", secret)
      .update(data)
      .digest("base64url");
  }

  // HMAC SHA384
  hmacSha384(data, secret) {
    return this.crypto
      .createHmac("sha384", secret)
      .update(data)
      .digest("base64url");
  }

  // HMAC SHA512
  hmacSha512(data, secret) {
    return this.crypto
      .createHmac("sha512", secret)
      .update(data)
      .digest("base64url");
  }

  // Валидация заголовка
  validateHeader(header) {
    if (!header.alg || !this.allowedAlgorithms.includes(header.alg)) {
      throw new Error("Invalid or unsupported algorithm");
    }

    if (!header.typ || header.typ !== "JWT") {
      throw new Error("Invalid token type");
    }
  }

  // Валидация payload
  validatePayload(payload) {
    if (!payload || typeof payload !== "object") {
      throw new Error("Invalid payload");
    }

    // Проверка на инъекцию
    if (this.containsInjection(payload)) {
      throw new Error("Payload contains malicious content");
    }
  }

  // Проверка на инъекцию
  containsInjection(obj) {
    const str = JSON.stringify(obj);

    // Проверка на XSS
    if (/<script/i.test(str)) {
      return true;
    }

    // Проверка на SQL инъекцию
    if (/union\s+select/i.test(str)) {
      return true;
    }

    // Проверка на команды
    if (/[;&|`$]/i.test(str)) {
      return true;
    }

    return false;
  }

  // Безопасное сравнение строк
  secureCompare(a, b) {
    if (a.length !== b.length) {
      return false;
    }

    let result = 0;
    for (let i = 0; i < a.length; i++) {
      result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }

    return result === 0;
  }

  // Base64 URL кодирование
  base64UrlEncode(str) {
    return Buffer.from(str)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=/g, ");
  }

  // Base64 URL декодирование
  base64UrlDecode(str) {
    str += "=".repeat((4 - (str.length % 4)) % 4);
    str = str.replace(/-/g, "+").replace(/_/g, "/");
    return Buffer.from(str, "base64").toString("utf8");
  }

  // Генерация JTI
  generateJTI() {
    return this.crypto.randomBytes(16).toString("hex");
  }
}

Заключение

JWT — это мощный инструмент для аутентификации и авторизации, но он требует правильной реализации и соблюдения мер безопасности. Ключевые принципы:

  • Правильный выбор алгоритма — используйте надежные алгоритмы
  • Безопасное управление секретами — храните секреты безопасно
  • Валидация токенов — проверяйте все аспекты токена
  • Защита от атак — используйте защитные механизмы

Помните: безопасность JWT — это не только правильная реализация, но и постоянное обновление знаний и следование лучшим практикам.


Совет: Начните с изучения основных уязвимостей JWT, затем внедрите соответствующие меры защиты в ваши приложения. Не забывайте о регулярном тестировании и обновлении мер безопасности!