Структура токенов, алгоритмы подписи, уязвимости 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, затем внедрите соответствующие меры защиты в ваши приложения. Не забывайте о регулярном тестировании и обновлении мер безопасности!