JSON Web Token(JWT)已成为现代 Web 应用中身份认证与授权的主流方案。它轻量、自包含、跨语言,看似简洁优雅的设计背后,却隐藏着一系列容易被开发者忽视的安全陷阱。本文将深入剖析 JWT 在实际部署中最常见的三类安全漏洞——算法混淆攻击、密钥爆破攻击以及 kid 注入攻击,并结合代码示例说明其原理与防御策略。
JWT 基础回顾
在讨论漏洞之前,有必要快速回顾 JWT 的结构。一个 JWT 由三部分组成,以点号分隔:
Header.Payload.Signature
- Header:Base64Url 编码的 JSON,通常包含
alg(签名算法)和typ字段。 - Payload:Base64Url 编码的 JSON,携带声明(claims),如
sub、exp、role等。 - Signature:对前两部分的签名,用于验证令牌未被篡改。
服务端验证 JWT 的核心逻辑是:根据 Header 中的 alg 字段选择对应算法,使用密钥重新计算签名,并与令牌中的签名比对。正是这个“信任 Header”的设计,为后续漏洞埋下了伏笔。
一、算法混淆攻击(Algorithm Confusion)
原理
算法混淆攻击的核心在于:服务端在验证 JWT 时,盲目信任 Header 中的 alg 字段,而没有在服务端强制指定允许的算法。
最常见的场景是 RS256 与 HS256 的混淆。RS256 使用非对称加密,服务端持有私钥签名、公钥验签;HS256 使用对称加密,签名与验签共用同一个密钥。
攻击者可以:
- 获取服务端用于 RS256 验签的公钥(公钥通常是公开的,可能暴露在 JWKS 端点或前端代码中)。
- 将 Header 中的
alg改为HS256。 - 使用该公钥作为 HMAC 密钥,对篡改后的 Payload 重新签名。
如果服务端代码逻辑是“根据 alg 动态选择验证方式”,且未限制算法白名单,就会用公钥去验证 HS256 签名——而攻击者恰好用同一公钥生成了这个签名,验证自然通过。
易受攻击的代码示例
const jwt = require('jsonwebtoken');
function verifyToken(token, publicKey) {
const decoded = jwt.decode(token, { complete: true });
const alg = decoded.header.alg;
// 危险:根据 header 中的 alg 动态选择验证方式
return jwt.verify(token, publicKey, { algorithms: [alg] });
}
防御措施
- 强制指定算法白名单:验证时显式传入
algorithms: ['RS256'],绝不允许从 Header 中读取算法。 - 使用成熟的 JWT 库,并确保其默认行为是安全的。
- 定期轮换密钥,降低密钥泄露后的影响面。
二、密钥爆破攻击(Key Brute-Forcing)
原理
当 JWT 使用 HS256 等对称算法时,安全性完全依赖于密钥的强度。如果开发者使用了弱密钥(如 secret、123456、password 或项目名称),攻击者可以离线对 JWT 进行暴力破解。
攻击流程非常简单:
- 获取一个合法的 JWT(例如通过注册账号登录获得)。
- 使用字典(如 rockyou.txt)或工具(如 hashcat、jwt_tool)对签名部分进行离线爆破。
- 一旦猜出密钥,即可伪造任意用户身份的 JWT。
由于 JWT 验证是纯计算操作,不涉及网络请求,攻击者可以在本地以极高速度尝试数百万个候选密钥。
实际案例
许多开源项目和教程示例中,密钥常被硬编码为:
const SECRET = 'mysecretkey';
这类密钥在字典攻击面前几乎瞬间沦陷。使用 hashcat 模式 16500,配合 GPU,每秒可尝试数十万次。
防御措施
- 使用高强度随机密钥:密钥长度至少 256 位,由密码学安全的随机数生成器产生。
- 优先使用非对称算法:RS256/ES256 的私钥不会分发给验证方,从根本上消除对称密钥爆破的风险。
- 定期轮换密钥,并支持多密钥验证以实现平滑过渡。
- 在密钥管理上使用 KMS(密钥管理服务)或 Vault 等专用工具。
三、kid 注入攻击(kid Injection)
原理
kid(Key ID)是 JWT Header 中的一个可选字段,用于指示服务端应该使用哪把密钥来验证签名。当服务端根据 kid 的值动态查找密钥时,如果未对输入做严格校验,就可能引入严重漏洞。
常见的 kid 注入攻击向量包括:
1. 路径遍历
服务端将 kid 拼接到文件路径中读取密钥文件:
const keyPath = `/keys/${header.kid}.pem`;
const key = fs.readFileSync(keyPath);
攻击者将 kid 设为 ../../../../dev/null,使服务端读取空文件作为密钥。此时攻击者用空字符串作为 HMAC 密钥签名,即可通过验证。
2. SQL 注入
服务端将 kid 拼入 SQL 查询:
SELECT key FROM keys WHERE id = '${header.kid}'
攻击者可以构造 kid 为 ' UNION SELECT 'attacker_key' --,使查询返回攻击者控制的密钥。
3. 命令注入
极端情况下,kid 被传入 shell 命令中,可能导致远程命令执行。
防御措施
- 永远不要信任 Header 中的任何字段,包括
kid。 - 对
kid进行严格的白名单校验,只允许预定义的合法值。 - 使用参数化查询访问数据库,避免 SQL 注入。
- 避免将
kid直接用于文件路径或系统命令。 - 如果必须动态查找密钥,使用映射表(如内存中的 Map)而非文件系统或数据库拼接。
综合防御建议
除了针对上述三类漏洞的具体措施,以下实践应作为 JWT 安全部署的基线:
- 算法白名单:服务端硬编码允许的算法,拒绝
none算法。 - 强制过期:设置合理的
exp,并使用nbf和iat辅助校验。 - 验证所有声明:不要只验证签名,还要校验
iss、aud等关键字段。 - 使用 JWKS 端点:对于非对称算法,通过标准 JWKS 端点分发公钥,并缓存与轮换。
- 安全审计与测试:使用
jwt_tool、jwt-hack等工具对自身系统进行渗透测试。 - 日志与监控:记录 JWT 验证失败事件,及时发现异常攻击行为。
结语
JWT 本身并非不安全,问题往往出在实现细节上。算法混淆、密钥爆破和 kid 注入这三类漏洞,本质上都源于对 Header 字段的过度信任以及对密钥管理的疏忽。作为开发者,理解这些攻击的原理,并在代码中贯彻“永不信任客户端输入”的原则,才能让 JWT 真正成为可靠的安全机制。安全从来不是某个库或某个协议的单一责任,而是贯穿设计、实现与运维的系统工程。
未经允许不得转载:任鹏个人博客 » JWT 安全漏洞剖析:算法混淆、密钥爆破与 kid 注入攻击

