JWT令牌伪造检测与后端安全合规错误码断言工程案例 - 第4节
4.1 攻击原理:JWT 签名验证的致命缺陷
alg=none 攻击是 JWT 安全中最基础、也最容易被忽视的漏洞。其核心原理在于:JWT 标准(RFC 7519)允许将头部中的 alg(算法)字段设置为 none,表示令牌不使用任何签名算法。当服务端的 JWT 解析库未对 alg 字段进行严格校验时,攻击者可以构造一个不含签名的 JWT,绕过签名验证逻辑,直接伪造任意用户身份。
攻击本质:服务端信任了客户端提供的算法声明,而非强制使用服务端预定义的签名算法。攻击者通过篡改 JWT 头部,将签名算法降级为
none,使签名验证步骤被跳过。
攻击流程可概括为以下步骤:
- 拦截合法 JWT:攻击者通过抓包或日志获取一个有效的 JWT。
- 篡改头部:将
alg字段从HS256或RS256修改为none。 - 伪造载荷:修改
payload中的用户身份信息(如sub、role),提升权限。 - 移除签名:将 JWT 的第三部分(签名)置空或删除。
- 发送恶意请求:将构造好的
header.payload.(注意末尾的点号)发送给服务端。
⚠️ 关键陷阱:许多开发者误以为“只要验证签名就能保证安全”,却忽略了
alg=none 这种“无签名”的例外情况。攻击者不需要知道任何密钥,仅凭对 JWT 结构的理解即可完成攻击。
4.2 Payload 构造:手写一个无签名 JWT
一个标准的 JWT 由三部分组成:header.payload.signature。在 alg=none 攻击中,攻击者需要构造一个 只有两部分 的令牌:header.payload.(末尾保留点号,签名部分为空)。
以下是一个典型的恶意 JWT 构造示例:
// 原始合法 JWT(使用 HS256 签名)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6InVzZXIifQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// 攻击者构造的 JWT(alg=none,无签名)
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkbWluIiwicm9sZSI6ImFkbWluIn0.
Payload 构造的关键点:
- 头部(Header):Base64URL 编码后的
{"alg":"none","typ":"JWT"}。 - 载荷(Payload):Base64URL 编码后的恶意用户信息,如
{"sub":"admin","role":"admin"}。 - 签名(Signature):完全省略,但 JWT 字符串末尾必须保留一个点号
.,表示签名部分为空。
💡 编码验证:可以使用
echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '=' 命令生成 Base64URL 编码的头部。注意 Base64URL 编码与标准 Base64 的区别:+ 替换为 -,/ 替换为 _,并去除末尾的 =。
4.3 服务端漏洞代码示例
以下代码展示了存在 alg=none 漏洞的典型服务端实现。漏洞根源在于:解析库从 JWT 头部读取 alg 字段,并直接使用该值进行验证,而非强制使用服务端预定义的算法。
// 漏洞代码示例(Node.js + jsonwebtoken 库)
const jwt = require('jsonwebtoken');
// 中间件:验证 JWT
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Missing token' });
}
try {
// 漏洞:未指定算法白名单,解析库从 token 头部读取 alg
const decoded = jwt.verify(token, 'my-secret-key');
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// 攻击者发送的 token:header.payload.
// jwt.verify() 发现 alg 为 none,直接跳过签名验证,返回解码后的 payload
// 攻击者成功以 admin 身份访问受保护资源
漏洞代码的详细分析:
| 代码行 | 问题描述 | 安全影响 |
|---|---|---|
jwt.verify(token, 'my-secret-key') |
未指定 algorithms 选项,库默认信任 token 头部的 alg 字段 |
攻击者可设置 alg: none,绕过签名验证 |
const decoded = jwt.verify(...) |
未校验返回的 decoded 对象是否包含预期字段 |
攻击者可伪造任意用户身份和权限 |
req.user = decoded |
直接将解码后的载荷赋值给请求对象,未做二次校验 | 恶意载荷中的 role: admin 被直接信任 |
⚠️ 常见错误认知:许多开发者认为“只要使用了 JWT 库,安全性就有保障”。实际上,
jsonwebtoken 库在 8.5.1 版本之前,默认行为就是信任 alg 头部。即使是最新版本,如果开发者显式不传 algorithms 参数,仍然可能受到攻击。
4.4 防御措施:强制算法白名单与输入校验
防御 alg=none 攻击的核心原则是:永远不要信任客户端提供的算法声明。服务端必须强制使用预定义的签名算法,并对 JWT 的完整性进行严格校验。
以下是最有效的防御措施:
- 强制算法白名单:在 JWT 验证时,显式指定允许的算法列表,拒绝
none及其他未授权算法。 - 严格校验 JWT 结构:确保 JWT 包含三个部分(header.payload.signature),拒绝只有两部分或格式异常的令牌。
- 使用安全的 JWT 库版本:升级到最新版本,并检查库的默认行为是否已修复
alg=none漏洞。 - 二次校验载荷内容:验证解码后的 payload 是否包含预期的字段和数据类型,防止注入恶意数据。
修复后的安全代码示例:
// 安全代码示例(Node.js + jsonwebtoken 库)
const jwt = require('jsonwebtoken');
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Missing token' });
}
try {
// 防御:强制指定算法白名单,拒绝 none
const decoded = jwt.verify(token, 'my-secret-key', {
algorithms: ['HS256'] // 只允许 HS256,拒绝 none、HS384 等
});
// 二次校验:确保载荷包含必要字段
if (!decoded.sub || !decoded.role) {
throw new Error('Invalid payload structure');
}
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
防御措施对比表:
| 防御措施 | 实现方式 | 效果 |
|---|---|---|
| 算法白名单 | algorithms: ['HS256'] |
直接拒绝 alg=none 及所有非白名单算法 |
| 结构校验 | 检查 JWT 是否包含三个部分,且签名部分非空 | 防止攻击者构造 header.payload. 格式 |
| 库版本升级 | 使用 jsonwebtoken@9+ 或等效安全库 |
利用库自身的安全修复,减少人为配置错误 |
| 载荷二次校验 | 验证 decoded.sub 是否为预期格式 |
即使绕过签名,也能通过业务逻辑拦截恶意数据 |
工程实践建议:在微服务架构中,建议将 JWT 验证逻辑封装为独立的中间件或网关服务,统一管理算法白名单和校验规则。同时,在 CI/CD 流水线中加入 JWT 安全扫描,自动检测代码中是否存在
alg=none 漏洞模式。