Skip to content

JWT 安全架构与防御全景 ​

JWT 防御的核心原则是不信任来自 Token Header 的任何控制指令,也不把无状态 Token 当作永久可信权限凭证。签发与验证双方必须由服务端配置决定算法、密钥来源与期望受众,任何对 alg、kid、jku、x5u、typ 或 role 的盲从都会演变为签名绕过或越权。

四大风险面与防御索引 ​

编号对应防御文档防御目标核心控制基线
0101-alg-none-defense.md无签名绕过服务端硬编码算法白名单,彻底禁止 none/空算法/算法数组
0202-algorithm-confusion-defense.mdRS/ES → HS 算法混淆密钥类型与算法强绑定,非对称公钥绝不进入对称 HMAC 分支
0303-weak-key-bruteforce-defense.md弱 HMAC 密钥爆破>= 256-bit CSPRNG 高熵密钥,或迁移至 RS256/ES256/EdDSA
0404-kid-injection-defense.mdkid 路径/SQL/命令注入kid 仅作静态映射表键或严格正则白名单查询,禁用拼接
0505-jku-x5u-abuse-defense.mdjku/x5u/x5c 密钥源劫持禁用 Token 内自带远程 URL,仅从本地静态配置的 JWKS 端点拉取
0606-claim-missing-defense.mdClaim 缺失与 Token 混用强制验证 exp/nbf/iss/aud/typ,敏感权限二次查库
0707-theft-replay-defense.md窃取、重放与持久化HttpOnly+SameSite Cookie、CSP 防 XSS、jti 吊销列表与 Refresh 轮换
0808-cve-library-defense.md依赖库 CVE 与解析差异锁定安全库版本、统一网关与业务解析器、禁止 key callback 返回空值
0909-toolchain-operations-defense.md工程化验证与发布门禁将 JWT 拒绝矩阵接入 CI/CD 回归套件,验证所有失败样本同源拒绝

防御工作流 ​

text
1. 入口格式预检(三段式结构 + 长度上限)
   └─ 拒绝非三段结构、空签名、过大 Header 或嵌套非法 JSON 类型
2. 服务端固定算法与密钥解析(Ignore Untrusted Header Directives)
   └─ 从服务端配置读取 EXPECTED_ALG 与静态 KeyStore;忽略 jku/x5u,严格白名单匹配 kid
3. 密码学验签与异常收敛
   └─ 使用常量时间比对或成熟库安全入口验签,统一返回通用 401,不泄露库指纹
4. 全量标准 Claim 校验
   └─ 强制校对 exp、nbf、iat、iss、aud 与 typ(例如区分 at+jwt 与 ID Token)
5. 状态撤销与上下文检查
   └─ 校验 jti 未在吊销黑名单中,敏感操作回源校验用户状态与最新角色权限

统一服务端 JWT 验证基线(Python) ​

python
import base64
import hashlib
import hmac
import json
import time
from typing import Any


class JWTVerificationError(Exception):
    """统一认证拒绝异常,对外只暴露通用 401 错误。"""


def _b64u_decode(segment: str) -> bytes:
    if not segment:
        raise JWTVerificationError("empty segment")
    padded = segment + "=" * (-len(segment) % 4)
    return base64.urlsafe_b64decode(padded.encode("ascii"))


def verify_hs256_baseline(
    token: str,
    keystore: dict[str, bytes],
    expected_iss: str,
    expected_aud: str,
    revoked_jtis: set[str],
    now: int | None = None,
) -> dict[str, Any]:
    """示范防御基线:固定 HS256、静态 kid 查表、强制完整 Claim 与吊销检查。"""
    parts = token.split(".")
    if len(parts) != 3 or not parts[2]:
        raise JWTVerificationError("malformed token or empty signature")
    h_b64, p_b64, s_b64 = parts

    try:
        header = json.loads(_b64u_decode(h_b64))
        payload = json.loads(_b64u_decode(p_b64))
    except Exception as exc:
        raise JWTVerificationError("invalid JSON encoding") from exc

    if not isinstance(header, dict) or not isinstance(payload, dict):
        raise JWTVerificationError("header/payload must be JSON objects")

    # 1. 严格锁定服务端期望算法与类型
    if header.get("alg") != "HS256":
        raise JWTVerificationError("algorithm mismatch")
    if header.get("typ") not in ("JWT", "at+jwt"):
        raise JWTVerificationError("unexpected token typ")
    if any(forbidden in header for forbidden in ("jku", "x5u", "jwk", "x5c")):
        raise JWTVerificationError("inline or remote key headers forbidden")

    # 2. kid 严格静态字典查找(拒绝路径/SQL/null/数组)
    kid = header.get("kid")
    if not isinstance(kid, str) or kid not in keystore:
        raise JWTVerificationError("unknown key id")
    secret = keystore[kid]
    if len(secret) < 32:
        raise JWTVerificationError("insufficient key length")

    # 3. 常量时间签名验证
    signing_input = f"{h_b64}.{p_b64}".encode("ascii")
    expected_sig = hmac.new(secret, signing_input, hashlib.sha256).digest()
    actual_sig = _b64u_decode(s_b64)
    if not hmac.compare_digest(expected_sig, actual_sig):
        raise JWTVerificationError("signature verification failed")

    # 4. 强制 Claim 校验
    ts = int(time.time()) if now is None else now
    for req in ("sub", "iss", "aud", "exp", "nbf", "jti"):
        if req not in payload:
            raise JWTVerificationError(f"missing required claim: {req}")
    if payload["iss"] != expected_iss:
        raise JWTVerificationError("issuer mismatch")
    aud = payload["aud"]
    if not (aud == expected_aud or (isinstance(aud, list) and expected_aud in aud)):
        raise JWTVerificationError("audience mismatch")
    if not isinstance(payload["exp"], (int, float)) or ts >= payload["exp"]:
        raise JWTVerificationError("token expired")
    if not isinstance(payload["nbf"], (int, float)) or ts < payload["nbf"]:
        raise JWTVerificationError("token not yet valid")
    if payload["jti"] in revoked_jtis:
        raise JWTVerificationError("token revoked")

    return payload

验证与判定标准 ​

运行下方测试脚本确认 00-overview.md 中的典型绕过路径(无签名、空签名、非法 kid、jku 注入、跨 aud/iss、过期与已吊销 jti)在基线验证器下全部失败:

python
def _b64u_encode(obj: dict[str, Any]) -> str:
    raw = json.dumps(obj, separators=(",", ":")).encode("utf-8")
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")


def run_overview_defense_checks() -> None:
    secret = b"0123456789abcdef0123456789abcdef"
    keystore = {"key-v1": secret}
    now = 1700000000
    good_header = {"alg": "HS256", "typ": "at+jwt", "kid": "key-v1"}
    good_payload = {
        "sub": "u-100",
        "iss": "https://auth.example.com",
        "aud": "api.example.com",
        "nbf": now - 10,
        "exp": now + 600,
        "jti": "jti-ok",
    }

    def sign(h: dict[str, Any], p: dict[str, Any]) -> str:
        msg = f"{_b64u_encode(h)}.{_b64u_encode(p)}"
        sig = base64.urlsafe_b64encode(hmac.new(secret, msg.encode(), hashlib.sha256).digest()).rstrip(b"=").decode()
        return f"{msg}.{sig}"

    valid_tok = sign(good_header, good_payload)
    assert verify_hs256_baseline(valid_tok, keystore, "https://auth.example.com", "api.example.com", set(), now)["sub"] == "u-100"

    bad_tokens = [
        f"{_b64u_encode({'alg': 'none', 'typ': 'at+jwt'})}.{_b64u_encode(good_payload)}.",
        sign({**good_header, "jku": "https://evil.example/jwks.json"}, good_payload),
        sign({**good_header, "kid": "../../dev/null"}, good_payload),
        sign(good_header, {**good_payload, "aud": "other-service"}),
        sign(good_header, {**good_payload, "exp": now - 1}),
        sign(good_header, {**good_payload, "jti": "jti-revoked"}),
    ]
    for tok in bad_tokens:
        try:
            verify_hs256_baseline(tok, keystore, "https://auth.example.com", "api.example.com", {"jti-revoked"}, now)
        except JWTVerificationError:
            pass
        else:
            raise AssertionError(f"bad token unexpectedly accepted: {tok}")

    print("PASS: all JWT overview defense checks passed")


if __name__ == "__main__":
    run_overview_defense_checks()

判定标准:

  1. 零 Header 控制面:所有包含 alg=none、jku/x5u/jwk/x5c 或未登记 kid 的 Token 统一返回 401。
  2. 完整生命周期封闭:过期、未生效、跨受众、跨签发者及已登出吊销 jti 均无法通过验证。
  3. 错误文本收敛:网关与服务端对外统一输出固定错误体与状态码,不回显库名称或解析堆栈。

MCP 工具映射 ​

防御阶段MCP 工具说明
防御路线选择kb_router按 JWT、alg:none、kid、jku、Claim 搜索对应子防御文档
规程读取kb_read_file读取 01–09 各篇针对性加固指南
目录编排kb_catalog核对 02-auth/jwt/ 下全部防御条目与配对关系

Apache-2.0 · 仅供授权环境下的学习与防御性研究使用 · 免责声明