JWT 安全漏洞剖析:算法混淆、密钥爆破与 kid 注入攻击

JSON Web Token(JWT)已成为现代 Web 应用中身份认证与授权的主流方案。它轻量、自包含、跨语言,看似简洁优雅的设计背后,却隐藏着一系列容易被开发者忽视的安全陷阱。本文将深入剖析 JWT 在实际部署中最常见的三类安全漏洞——算法混淆攻击、密钥爆破攻击以及 kid 注入攻击,并结合代码示例说明其原理与防御策略。

JWT 基础回顾

在讨论漏洞之前,有必要快速回顾 JWT 的结构。一个 JWT 由三部分组成,以点号分隔:

Header.Payload.Signature
  • Header:Base64Url 编码的 JSON,通常包含 alg(签名算法)和 typ 字段。
  • Payload:Base64Url 编码的 JSON,携带声明(claims),如 subexprole 等。
  • Signature:对前两部分的签名,用于验证令牌未被篡改。

服务端验证 JWT 的核心逻辑是:根据 Header 中的 alg 字段选择对应算法,使用密钥重新计算签名,并与令牌中的签名比对。正是这个“信任 Header”的设计,为后续漏洞埋下了伏笔。

一、算法混淆攻击(Algorithm Confusion)

原理

算法混淆攻击的核心在于:服务端在验证 JWT 时,盲目信任 Header 中的 alg 字段,而没有在服务端强制指定允许的算法。

最常见的场景是 RS256 与 HS256 的混淆。RS256 使用非对称加密,服务端持有私钥签名、公钥验签;HS256 使用对称加密,签名与验签共用同一个密钥。

攻击者可以:

  1. 获取服务端用于 RS256 验签的公钥(公钥通常是公开的,可能暴露在 JWKS 端点或前端代码中)。
  2. 将 Header 中的 alg 改为 HS256
  3. 使用该公钥作为 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 等对称算法时,安全性完全依赖于密钥的强度。如果开发者使用了弱密钥(如 secret123456password 或项目名称),攻击者可以离线对 JWT 进行暴力破解。

攻击流程非常简单:

  1. 获取一个合法的 JWT(例如通过注册账号登录获得)。
  2. 使用字典(如 rockyou.txt)或工具(如 hashcat、jwt_tool)对签名部分进行离线爆破。
  3. 一旦猜出密钥,即可伪造任意用户身份的 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 安全部署的基线:

  1. 算法白名单:服务端硬编码允许的算法,拒绝 none 算法。
  2. 强制过期:设置合理的 exp,并使用 nbfiat 辅助校验。
  3. 验证所有声明:不要只验证签名,还要校验 issaud 等关键字段。
  4. 使用 JWKS 端点:对于非对称算法,通过标准 JWKS 端点分发公钥,并缓存与轮换。
  5. 安全审计与测试:使用 jwt_tooljwt-hack 等工具对自身系统进行渗透测试。
  6. 日志与监控:记录 JWT 验证失败事件,及时发现异常攻击行为。

结语

JWT 本身并非不安全,问题往往出在实现细节上。算法混淆、密钥爆破和 kid 注入这三类漏洞,本质上都源于对 Header 字段的过度信任以及对密钥管理的疏忽。作为开发者,理解这些攻击的原理,并在代码中贯彻“永不信任客户端输入”的原则,才能让 JWT 真正成为可靠的安全机制。安全从来不是某个库或某个协议的单一责任,而是贯穿设计、实现与运维的系统工程。

未经允许不得转载:任鹏个人博客 » JWT 安全漏洞剖析:算法混淆、密钥爆破与 kid 注入攻击

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏