JWT 依赖库 CVE 治理与安全调用防御
JWT 库漏洞通常来自两方面:一是使用含历史 CVE 的旧版本库(如放行 alg=none、PEM 公钥误作 HMAC 密钥、未限制的 kid/对象回调路径),二是业务代码在安全版本上误用了高危 API 或编写了有缺陷的 key callback(例如未校验回调返回值非空、信任对象原型属性、或对外抛出暴露库名与版本的异常栈)。
各语言 JWT 库安全基线与高危 API 替换
| 语言 / 生态 | 常见脆弱包或版本 | 推荐升级基线 | 必须消除的危险调用模式 |
|---|---|---|---|
| Node.js | jsonwebtoken < 9.0.0, express-jwt < 7 | jsonwebtoken >= 9.0.0 或 jose >= 5 | 缺失 algorithms 参数、secretOrPublicKey 回调函数返回 null/undefined 或非字符串对象 |
| Python | PyJWT < 2.4.0, 未维护的 python-jwt/pyjwt 旧版 | PyJWT >= 2.8.0 / authlib >= 1.3.0 | jwt.decode(..., verify=False)、options={"verify_signature": False}、未传 algorithms=[...] |
| Java | jjwt < 0.11.5 | io.jsonwebtoken >= 0.12.5 / nimbus-jose-jwt >= 9.37 | 使用允许未签名 JWT 的 parseClaimsJwt() 替代 parseSignedClaims() |
| Go | github.com/dgrijalva/jwt-go(已归档含漏洞) | github.com/golang-jwt/jwt/v5 >= v5.2.0 | Keyfunc 回调函数中未校验 token.Method 算法族直接返回密钥 |
| PHP | firebase/php-jwt < 6.0.0 | firebase/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()判定标准:
- 依赖零陈旧:所有 lockfile 中的 JWT 验证库均不低于安全基线版本,且已移除
dgrijalva/jwt-go等归档漏洞包。 - 错误指纹零泄露:向认证接口传入
abc.def.ghi、alg=none、错误kid、过期 Token 时,响应中不出现secretOrPublicKey、jwt malformed、InvalidAlgorithmError、io.jsonwebtoken等库特征文本,且响应体哈希一致。 - 回调永不返回空:所有自定义
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/ 中的依赖加固文档 |