Skip to content

JWT 依赖库 CVE 治理与安全调用防御 ​

JWT 库漏洞通常来自两方面:一是使用含历史 CVE 的旧版本库(如放行 alg=none、PEM 公钥误作 HMAC 密钥、未限制的 kid/对象回调路径),二是业务代码在安全版本上误用了高危 API 或编写了有缺陷的 key callback(例如未校验回调返回值非空、信任对象原型属性、或对外抛出暴露库名与版本的异常栈)。

各语言 JWT 库安全基线与高危 API 替换 ​

语言 / 生态常见脆弱包或版本推荐升级基线必须消除的危险调用模式
Node.jsjsonwebtoken < 9.0.0, express-jwt < 7jsonwebtoken >= 9.0.0 或 jose >= 5缺失 algorithms 参数、secretOrPublicKey 回调函数返回 null/undefined 或非字符串对象
PythonPyJWT < 2.4.0, 未维护的 python-jwt/pyjwt 旧版PyJWT >= 2.8.0 / authlib >= 1.3.0jwt.decode(..., verify=False)、options={"verify_signature": False}、未传 algorithms=[...]
Javajjwt < 0.11.5io.jsonwebtoken >= 0.12.5 / nimbus-jose-jwt >= 9.37使用允许未签名 JWT 的 parseClaimsJwt() 替代 parseSignedClaims()
Gogithub.com/dgrijalva/jwt-go(已归档含漏洞)github.com/golang-jwt/jwt/v5 >= v5.2.0Keyfunc 回调函数中未校验 token.Method 算法族直接返回密钥
PHPfirebase/php-jwt < 6.0.0firebase/php-jwt >= 6.10.0允许传入多种算法键混用的 $decoded 映射或忽略 Key 类型封装

防御工作流 ​

text
1. 依赖清单版本基线检查
   └─ 扫描 package-lock.json / requirements.txt / go.mod / pom.xml / composer.lock,阻断已知受影响区间上线
2. Header Schema 强类型预检(防 Node/JS 原型与类型混淆)
   └─ 在调用库函数前校验 alg、kid、typ 必须为原始字符串(拒绝 null、array、object 如 {"toString":...})
3. key callback 故障安全封装(Fail-Closed Key Lookup)
   └─ 动态密钥回调仅在精确命中白名单后返回非空强类型密钥;未命中立即显式抛出异常,严禁返回 None/null/undefined
4. 统一错误边界与响应收敛
   └─ 拦截库内部抛出的 JsonWebTokenError / InvalidAlgorithmError / JwtException,对外一律映射为固定 JSON {"error":"unauthorized"}
5. 统一网关与后端解析栈
   └─ 消除“网关用 Go 库解析、业务用 Node 库解析”导致的嵌套/空白/大小写解析偏差

Python Header 预检与依赖基线检查脚本 ​

python
import base64
import json
import re
from typing import Any

# 各主流 JWT 库最小安全主次版本基线(tuple[major, minor, patch])
MIN_SAFE_VERSIONS: dict[str, tuple[int, int, int]] = {
    "jsonwebtoken": (9, 0, 0),
    "pyjwt": (2, 8, 0),
    "python-jose": (3, 3, 0),
    "golang-jwt/jwt": (5, 2, 0),
    "firebase/php-jwt": (6, 10, 0),
}

DEPRECATED_PACKAGES = {"dgrijalva/jwt-go"}


def parse_semver(raw: str) -> tuple[int, int, int]:
    nums = [int(x) for x in re.findall(r"\d+", raw)[:3]]
    while len(nums) < 3:
        nums.append(0)
    return (nums[0], nums[1], nums[2])


def check_dependency_baseline(packages: list[tuple[str, str]]) -> list[str]:
    """核查依赖版本是否达到安全基线,返回不合规项列表。"""
    issues: list[str] = []
    for name, ver in packages:
        low = name.lower()
        if any(dep in low for dep in DEPRECATED_PACKAGES):
            issues.append(f"deprecated vulnerable package: {name} {ver}")
            continue
        for pkg, min_ver in MIN_SAFE_VERSIONS.items():
            if pkg in low and parse_semver(ver) < min_ver:
                issues.append(f"outdated JWT library {name}=={ver} < {'.'.join(map(str, min_ver))}")
    return issues


def safe_jwt_header_preflight(token: str) -> dict[str, Any]:
    """在交给第三方 JWT 库前阻止 null/数组/对象原型污染等畸形 Header。"""
    parts = token.split(".")
    if len(parts) != 3 or not all(parts):
        raise ValueError("malformed token")
    padded = parts[0] + "=" * (-len(parts[0]) % 4)
    header = json.loads(base64.urlsafe_b64decode(padded.encode("ascii")))
    if not isinstance(header, dict):
        raise ValueError("header must be JSON object")

    for str_field in ("alg", "typ", "kid"):
        if str_field in header:
            val = header[str_field]
            if not isinstance(val, str) or not val.strip():
                raise ValueError(f"header field {str_field} must be non-empty string")
            if str_field in ("__proto__", "constructor", "prototype"):
                raise ValueError("forbidden key")

    if any(k in header for k in ("__proto__", "constructor", "prototype")):
        raise ValueError("prototype pollution key rejected")
    return header


def safe_key_callback(kid: Any, keystore: dict[str, bytes]) -> bytes:
    """安全的 key callback:绝不返回 None/空值,未命中直接拒绝。"""
    if not isinstance(kid, str) or kid not in keystore:
        raise PermissionError("key lookup failed")
    key = keystore[kid]
    if not key:
        raise PermissionError("empty key rejected")
    return key

验证与判定标准 ​

运行下方脚本验证 08-cve-library.md 中的旧库检出、Node key callback 空值/数组/对象({"toString":...})注入及畸形 Header 均被拦截:

python
def run_cve_library_defense_checks() -> None:
    issues = check_dependency_baseline([
        ("jsonwebtoken", "8.5.1"),
        ("PyJWT", "2.1.0"),
        ("github.com/dgrijalva/jwt-go", "3.2.0"),
        ("PyJWT", "2.9.0"),
    ])
    assert len(issues) == 3, f"expected 3 flagged libraries, got {issues}"

    def make_tok(h: dict[str, Any]) -> str:
        hb = base64.urlsafe_b64encode(json.dumps(h).encode()).rstrip(b"=").decode()
        pb = base64.urlsafe_b64encode(b'{"sub":"u"}').rstrip(b"=").decode()
        return f"{hb}.{pb}.sig"

    bad_headers = [
        {"alg": "HS256", "kid": ""},
        {"alg": "HS256", "kid": None},
        {"alg": "HS256", "kid": ["missing"]},
        {"alg": "HS256", "kid": {"toString": "missing"}},
        {"alg": "HS256", "typ": ["JWT"]},
        {"alg": "HS256", "__proto__": {"polluted": True}},
    ]
    for h in bad_headers:
        try:
            safe_jwt_header_preflight(make_tok(h))
        except ValueError:
            pass
        else:
            raise AssertionError(f"malformed header bypassed preflight: {h}")

    try:
        safe_key_callback(None, {"k1": b"secret"})
    except PermissionError:
        pass
    else:
        raise AssertionError("None kid did not fail closed")

    assert safe_jwt_header_preflight(make_tok({"alg": "RS256", "typ": "at+jwt", "kid": "k1"}))["kid"] == "k1"
    print("PASS: all JWT library CVE & callback defense checks passed")


if __name__ == "__main__":
    run_cve_library_defense_checks()

判定标准:

  1. 依赖零陈旧:所有 lockfile 中的 JWT 验证库均不低于安全基线版本,且已移除 dgrijalva/jwt-go 等归档漏洞包。
  2. 错误指纹零泄露:向认证接口传入 abc.def.ghi、alg=none、错误 kid、过期 Token 时,响应中不出现 secretOrPublicKey、jwt malformed、InvalidAlgorithmError、io.jsonwebtoken 等库特征文本,且响应体哈希一致。
  3. 回调永不返回空:所有自定义 key callback 在 kid 缺失或为非字符串类型时直接抛错退出。

MCP 工具映射 ​

防御阶段MCP 工具说明
防御知识定位kb_router按 CVE、jsonwebtoken、PyJWT、jjwt 搜索防御条目
规程读取kb_read_file读取 08-cve-library-defense 与 01-alg-none-defense
目录索引核查kb_catalog核对 02-auth/jwt/ 及 09-cve/ 中的依赖加固文档

Apache-2.0 · For authorized learning and defensive research only · Disclaimer