Token 与 JWT 漏洞:无签名验证、alg 混淆攻击、敏感信息泄露、Token 泄露与重放
JWT 这东西,现在几乎成了微服务认证的标配。但说实话,我见过太多团队把 JWT 当成「万能令牌」,签发了就往客户端一丢,觉得万事大吉。结果呢?漏洞一个接一个。
这一章,咱们就专门聊聊 JWT 最常见的四个坑:无签名验证、alg 混淆攻击、敏感信息泄露、Token 泄露与重放。每个坑我都踩过,或者帮别人填过。
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),让服务端去查真正的数据。
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)+ 一次性使用 |
jti 字段,服务端维护一个已使用 jti 的黑名单,防止重放。
4.5 知识体系:JWT 攻击面全景图
下面这张图,是我自己梳理的 JWT 攻击面全景。每次做安全评审时,我都会对照这张图逐项检查。
4.6 实战检查清单
最后,给你一份我每次做 JWT 安全评审时都会用的检查清单。你可以直接拿去用:
- 签名验证:是否使用了
jwt.verify()而不是jwt.decode()?密钥是否硬编码在代码里? - 算法白名单:服务端是否硬编码了允许的算法列表?是否拒绝了
none算法? - 敏感数据:payload 里是否包含手机号、身份证、密码等敏感信息?是否使用了 JWE 加密?
- 传输安全:Token 是否只通过 HTTPS 传输?是否放在了 Authorization header 里而不是 URL 参数?
- 存储安全:前端 Token 是否存储在 HttpOnly Cookie 中?是否避免了 localStorage 存储?
- 生命周期:Access Token 的过期时间是否足够短(建议 15 分钟以内)?是否使用了 Refresh Token?
- 重放防护:是否引入了
jti字段?服务端是否维护了已使用 jti 的黑名单?
alg 为 none,再改 payload 里的 role 为 admin,看看服务端会不会放行。这一步能发现 80% 的 JWT 漏洞。
嗯,JWT 的坑就这些。说白了,它就是个带签名的 JSON,不是保险箱。别把不该放的东西放进去,别让攻击者替你选算法,别让 token 活太久。做到这三点,基本就稳了。