乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • JWT 基础

    • JWT 概述
    • JWT 结构详解
  • 签名与加密

    • JWS 签名基础
    • RS256与ES256签名
    • JWE 加密
  • 密钥管理

    • JWK 与密钥管理
  • 算法原理

    • 核心算法详解
  • 实战与安全

    • JWT 实际应用
    • JWT 安全攻防

JWT 安全攻防

广州飞翔科技(learnto.cn)技术团队内部教程 | 作者:白歌(架构师)审核:大翔(CEO/CTO)

JWT 的简洁性让开发者容易低估其安全风险。本文系统梳理 JWT 的常见攻击手法与防御策略,并穿插白歌对飞翔科技系统的安全审计实录。


"alg: none" 攻击:算法剥离

攻击原理

JWT 头部中的 alg 声明告诉验证方该用什么算法验签。某些早期库直接根据这个声明选择验证算法。攻击者把 alg 改成 none,并删除签名部分:

// 原始令牌头部
{ "alg": "HS256", "typ": "JWT" }

// 攻击者篡改后
{ "alg": "none", "typ": "JWT" }

编码后令牌变成 header.payload.(注意最后没有签名)。如果库看到 alg: none 就直接跳过验证,攻击者就能随意篡改 Payload 中的权限声明,实现 权限提升 (Privilege Escalation)。

飞翔科技审计实录

白歌在审计乐途旧版管理后台时发现,一个内部工具用的 Node.js 库允许 alg: none:

// 危险代码示例(已修复)
const decoded = jwt.verify(token, secret); // 库自动读取 alg 字段

白歌立即在飞书群里 @ 小崔:"这个库版本有 CVE,升级到新版本,并且咱们所有服务必须显式传算法参数。"

// 安全写法
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });

RS256 公钥作为 HS256 密钥:算法混淆

攻击原理

许多库的验证 API 长这样:

function jwtDecode(token, secretOrPublicKey) { ... }

攻击者拿到一个 RS256 签名的 JWT 和对应的 公钥 (公钥本来就是公开的),然后:

  1. 把头部 alg 从 RS256 改成 HS256
  2. 用公钥字符串作为 HS256 的共享密钥重新签名
  3. 发给服务端

服务端看到 alg: HS256,就把第二个参数当共享密钥用,结果公钥字符串恰好能验过攻击者伪造的 HMAC 签名!

防御措施

  • 显式算法 :调用验证函数时强制指定 algorithms: ['RS256']
  • 密钥分离 :RS256 的公钥和 HS256 的共享密钥走不同的配置通道,绝不混用同一个参数

弱 HMAC 密钥:暴力破解

为什么密码不能当密钥?

HMAC 共享密钥(Shared Secret)针对速度优化,这意味着暴力破解(Brute-force Attack)比破解 bcrypt 密码哈希快得多。规范规定:

HS256 的密钥长度 至少 256 位 (32 字节),即最少 32 个 ASCII 字符。

飞翔科技审计实录

白歌用密钥扫描工具检查乐途各服务的 JWT 配置,发现测试环境有个服务用了 secret 当密钥:

// 危险!只有6个字符
const SECRET = 'secret';

白歌在审计报告里标红:"这个密钥用普通笔记本 2 小时就能暴力破解。生产环境必须用密码学安全的随机生成器(CSPRNG, Cryptographically Secure Pseudo-Random Number Generator)生成至少 256 位密钥。"

李眉(运维)随后把密钥管理迁移到 HashiCorp Vault,由 Vault 自动生成 512 位随机密钥。


错误的加密+签名假设

加密不等于防篡改

很多开发者误以为"数据加密了,攻击者看不懂,也就改不了"。这是致命错误。某些加密算法(如 CBC 模式)在密文被篡改后仍会输出"看似合理"的明文,攻击者可以通过精心修改密文位来翻转解密后的布尔值。

嵌套 JWT 的验证陷阱

规范支持 嵌套 JWT (Nested JWT):外层加密、内层签名。常见错误是只解密了外层,看到内层"像 JWT"就直接用,跳过内层签名验证。

飞翔科技场景 :飞翔科技向第三方合作伙伴发送嵌套 JWT,外层用合作伙伴公钥加密,内层用乐途私钥签名。白歌在对接文档里用加粗红字强调:"必须验证内层签名! 只解密不验签等于把伪造数据当圣旨。"


无效椭圆曲线攻击

输入验证缺失的后果

椭圆曲线密码学(ECC)的所有运算都必须在曲线上的有效点进行。如果库没有验证公钥是否是有效曲线点,攻击者可以构造特殊输入,让标量乘法产生异常结果,最终 泄露私钥 。

防御

  • 使用经过实战检验的库(如 Node.js 的 jsonwebtoken、Java 的 jjwt)
  • 确保库在验签前调用 isValidPoint() 之类的检查
  • 只使用标准曲线:P-256、P-384、P-521

替换攻击:令牌张冠李戴

不同接收者攻击

攻击者把发给 API A 的令牌拿去调用 API B。如果两个服务共享同一套密钥且只验签名不验受众,攻击者就能在 B 系统冒用 A 系统的权限。

防御 :每个令牌必须包含 aud(受众,Audience)声明,服务端严格校验精确匹配。

跨 JWT 攻击(相同接收者)

飞翔科技有两个服务:user-database 和 item-database。item-database 团队升级时偷懒,把 aud 验证写成"包含 cool-company 即可",而不是精确匹配 cool-company/item-database。结果发给 user-database 的令牌也能操作 item-database!

白歌在代码评审时抓到这个漏洞,在 GitLab 上留了评论:"aud 验证必须是全等比较(===),不能用 includes()。这是跨 JWT 攻击的经典入口。"


缓解措施与最佳实践(8 条)

基于业界 JWT 最佳实践,白歌为飞翔科技制定了以下安全基线:

始终执行算法验证

绝不依赖 JWT 头部中的 alg 声明选择验证算法。代码里写死允许的算法白名单。

// 正确
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

// 错误
jwt.verify(token, publicKey); // 让库自己决定

使用适当的算法

  • 单体内部服务:HS256
  • 联邦身份 / 第三方接入:RS256 / ES256
  • 拒绝接受不安全的算法(如 none)

始终执行所有验证

嵌套 JWT 必须逐层验证。外层解密后,内层签名必须再验一次。

始终验证加密输入

椭圆曲线公钥必须是有效曲线点;RSA 模数必须满足长度和格式要求。

选择强密钥

  • HMAC 密钥 ≥ 256 位,且完全随机
  • RSA 密钥 ≥ 2048 位
  • ECC 使用 P-256 及以上标准曲线
  • 密钥由 CSPRNG 或硬件随机数生成器(HSM)生成

验证所有可能的声明

声明含义验证要求
exp过期时间(Expiration Time)必须存在,必须未过期
iat签发时间(Issued At)不能是未来时间
nbf生效时间(Not Before)必须已生效
iss签发者(Issuer)必须来自信任的签发者
aud受众(Audience)必须精确匹配本服务
sub主题(Subject)必须存在且合法

使用 typ 声明区分令牌类型

如果系统同时处理 Access Token、Refresh Token、ID Token,用 typ 或自定义声明区分,防止一种令牌被当另一种用。

为每种令牌使用不同的验证规则

  • 不同子系统使用不同的密钥对签名
  • iss 声明精确到子系统 URL,而非笼统的公司名
  • 避免"一把私钥签所有令牌"

结语

JWT 的安全问题大多出在 实现层面 ,而非规范本身。白歌在飞翔科技的安全审计中发现的漏洞,无一例外都是"图省事"或"想当然"造成的:依赖 alg 声明、密钥太短、aud 用模糊匹配、嵌套 JWT 只解密不验签……

记住两条铁律:

  1. 不要自己实现加密算法(Don't roll your own crypto)
  2. 验证一切可以验证的(Validate everything that can be validated)

JWT 是把好刀,但刀口朝向自己还是朝向敌人,取决于使用它的人是否懂行。

上一页
JWT 实际应用