Уязвимости, атаки, защита, 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 и микросервисными архитектурами.