Token 与 JWT 漏洞:无签名验证、alg 混淆攻击、敏感信息泄露、Token 泄露与重放

JWT 这东西,现在几乎成了微服务认证的标配。但说实话,我见过太多团队把 JWT 当成「万能令牌」,签发了就往客户端一丢,觉得万事大吉。结果呢?漏洞一个接一个。

这一章,咱们就专门聊聊 JWT 最常见的四个坑:无签名验证、alg 混淆攻击、敏感信息泄露、Token 泄露与重放。每个坑我都踩过,或者帮别人填过。

核心认知: JWT 的安全不取决于算法本身,而取决于你如何验证它。签名没验对,等于没锁门。

4.1 无签名验证:最基础的致命错误

先看一个我实际审计过的代码片段:

// 错误示例:只解码,不验证签名
function authenticate(token) {
    const payload = jwt.decode(token);  // 直接解码
    const user = findUser(payload.sub);
    return user;
}

这段代码的问题在哪?jwt.decode() 只是把 Base64 编码的 payload 解出来,它根本不检查签名。攻击者可以自己伪造一个 token,把 sub 改成 admin,然后直接传过来。服务端一解,嗯,你是 admin,放行。

我遇到过一家做 IoT 平台的公司,他们的设备认证就用了这种写法。攻击者抓到一个普通设备的 token,改了 payload 里的设备 ID,直接冒充成了网关设备。后果?整个内网拓扑都暴露了。

避坑指南: 我曾经在代码审查时发现,有人把 jwt.verify() 的第二个参数(密钥)传成了 null。结果 verify 函数直接跳过了签名校验。记住:jwt.verify(token, secret) 才是正确的用法,decode 只用于调试。

4.2 alg 混淆攻击:当攻击者替你选了算法

这个漏洞更隐蔽。JWT 的 header 里有个 alg 字段,告诉服务端「我用什么算法签的名」。如果服务端不加校验,攻击者可以把 alg 改成 none,或者把非对称算法 RS256 改成对称算法 HS256。

举个例子:

// 原始 token(RS256 签名)
header: {"alg": "RS256"}
payload: {"sub": "user", "role": "member"}

// 攻击者篡改后(HS256,用公钥作为密钥)
header: {"alg": "HS256"}
payload: {"sub": "user", "role": "admin"}
signature: HMAC(公钥, header.payload)

为什么能成功?因为很多 JWT 库在验证 HS256 时,会用你传进去的「密钥」直接做 HMAC。而公钥通常是公开的。攻击者拿到公钥,用它当 HMAC 密钥,重新签名一个 token。服务端一验:嗯,签名对得上。放行。

攻击类型 原始 alg 篡改后 alg 利用方式
none 攻击 RS256 / HS256 none 直接去掉签名,服务端不校验
算法混淆 RS256 HS256 用公开的公钥作为 HMAC 密钥重新签名
算法降级 RS256 HS256(弱密钥) 暴力破解弱密钥后伪造 token
我的习惯: 在服务端硬编码允许的算法列表,比如只接受 RS256。任何 token 的 alg 不在这个列表里,直接拒绝。别让攻击者替你选算法。

4.3 敏感信息泄露:JWT 不是加密的

很多人以为 JWT 是加密的。其实不是。JWT 只是 Base64 编码,不是加密。谁都能解码看内容。

我见过一个真实案例:某电商平台的 token 里,直接放了用户的手机号、身份证号、甚至信用卡后四位。攻击者只要抓个包,解码 token,就能拿到这些敏感信息。

// 解码后的 payload(明文)
{
  "sub": "1234567890",
  "name": "张三",
  "phone": "13800138000",
  "id_card": "110101199001011234",
  "credit_card_last4": "1234"
}

你想想看,这等于把用户的隐私写在额头上到处走。JWT 的设计初衷是「声明传递」,不是「数据加密」。如果你需要在 token 里放敏感数据,必须用 JWE(JSON Web Encryption)做加密,或者干脆只放一个不可逆的标识符(比如 session ID),让服务端去查真正的数据。

避坑指南: 我曾经在审计一个金融 App 时,发现他们把用户的交易密码 hash 放在了 token 里。虽然 hash 了,但攻击者可以离线暴力破解。记住:JWT 的 payload 里,永远不要放密码、密钥、身份证号、信用卡号等敏感信息。

4.4 Token 泄露与重放:偷一次,用一辈子

Token 泄露的途径太多了:HTTPS 没配好、前端日志里打印了 token、URL 参数里传 token(被 Referer 泄露)、本地存储被 XSS 偷走……一旦泄露,攻击者就能拿着这个 token 冒充用户,直到 token 过期。

更麻烦的是重放攻击。攻击者不需要知道 token 的内容,只需要把 token 原样再发一次。如果服务端没有做重放检测,攻击者可以反复使用同一个 token 做操作。

我遇到过最离谱的一次:某系统把 token 放在 URL 的 query 参数里,用户分享链接时,token 也跟着分享出去了。结果 A 用户把链接发到群里,B 用户打开链接,直接登录了 A 的账号。

泄露途径 风险等级 防护建议
HTTPS 未启用 高 全站强制 HTTPS,HSTS
前端日志打印 中 禁止在生产环境打印 token
URL 参数传递 高 使用 Authorization header,不用 URL
本地存储被 XSS 窃取 高 使用 HttpOnly Cookie 存储 token
重放攻击 中 引入 jti(JWT ID)+ 一次性使用
我的建议: 对于高安全场景,使用短生命周期的 access token(比如 15 分钟),配合 refresh token。同时引入 jti 字段,服务端维护一个已使用 jti 的黑名单,防止重放。

4.5 知识体系:JWT 攻击面全景图

下面这张图,是我自己梳理的 JWT 攻击面全景。每次做安全评审时,我都会对照这张图逐项检查。

JWT 攻击面全景图 JWT Token 无签名验证 alg 混淆攻击 敏感信息泄露 Token 泄露与重放 攻击细节 • 使用 decode() 代替 verify() • 密钥传 null 跳过校验 • 伪造任意 payload 攻击细节 • 篡改 alg 为 none • RS256 → HS256 混淆 • 用公钥做 HMAC 密钥 攻击细节 • Base64 解码即明文 • 手机号/身份证泄露 • 密码 hash 可暴力破解 攻击细节 • URL 参数泄露 • XSS 窃取 localStorage • 重放攻击无检测 防护措施:硬编码算法列表 + 强制 verify() + 敏感数据加密 + 短生命周期 + jti 防重放 安全评审对照清单 每次上线前,逐项检查这四个攻击面

4.6 实战检查清单

最后,给你一份我每次做 JWT 安全评审时都会用的检查清单。你可以直接拿去用:

  1. 签名验证:是否使用了 jwt.verify() 而不是 jwt.decode()?密钥是否硬编码在代码里?
  2. 算法白名单:服务端是否硬编码了允许的算法列表?是否拒绝了 none 算法?
  3. 敏感数据:payload 里是否包含手机号、身份证、密码等敏感信息?是否使用了 JWE 加密?
  4. 传输安全:Token 是否只通过 HTTPS 传输?是否放在了 Authorization header 里而不是 URL 参数?
  5. 存储安全:前端 Token 是否存储在 HttpOnly Cookie 中?是否避免了 localStorage 存储?
  6. 生命周期:Access Token 的过期时间是否足够短(建议 15 分钟以内)?是否使用了 Refresh Token?
  7. 重放防护:是否引入了 jti 字段?服务端是否维护了已使用 jti 的黑名单?
我的习惯: 每次上线前,我都会用 Burp Suite 抓一个 token,然后手动改 alg 为 none,再改 payload 里的 role 为 admin,看看服务端会不会放行。这一步能发现 80% 的 JWT 漏洞。

嗯,JWT 的坑就这些。说白了,它就是个带签名的 JSON,不是保险箱。别把不该放的东西放进去,别让攻击者替你选算法,别让 token 活太久。做到这三点,基本就稳了。