JWT安全漏洞之签名验证缺失攻击详解
一、 题目/知识点描述
JWT签名验证缺失攻击,是指JSON Web Token的接收方(通常是服务器端)在验证JWT令牌时,没有正确验证其签名的完整性,导致攻击者可以篡改令牌的有效载荷(Payload)部分(例如,用户ID、角色权限等),而验证方会无条件信任这些被篡改的数据。这是一种高危漏洞,可导致越权访问、权限提升等安全问题。
简单比喻:这就像收到一封盖了“已检验”印章的官方文件,我们只看印章就相信了文件内容。但如果这个文件本身可以被任意涂改,而印章的“检验”功能(即签名验证)被我们忽略掉了,那么伪造一份高权限的文件就变得轻而易举。
二、 解题过程/知识讲解
我们将从JWT的基本结构入手,逐步分析漏洞的成因、利用方法和防御措施。
步骤1:回顾JWT的基本结构
JWT(JSON Web Token)是一种开放标准,用于在各方之间安全地传输信息。一个完整的JWT由三部分组成,以点号分隔:
- 头部:包含令牌类型和签名算法,如
{"alg":"HS256","typ":"JWT"},然后进行Base64Url编码。 - 载荷:存放实际需要传递的声明(Claims),如
{"user":"alice","role":"user","exp":1672500000},然后进行Base64Url编码。 - 签名:对编码后的头部和载荷,使用一个密钥和头部指定的算法(如HS256)进行签名,以确保令牌的完整性和真实性。
一个典型的JWT如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciIsImV4cCI6MTY3MjUwMDAwMH0.xxxxxx
步骤2:理解JWT验证的常规流程
当服务器收到一个JWT时,应该执行以下步骤进行验证:
- 格式验证:检查令牌是否由三部分组成,用点号分隔。
- 头部解析:解码头部,获取
alg字段,确认签名算法。 - 载荷解析:解码载荷,检查有效期、颁发者等声明。
- 核心步骤:签名验证:使用与颁发方约定的正确密钥和算法,重新计算前两部分(Header.Payload)的签名。将计算出的新签名与JWT提供的第三部分(签名)进行比较。两者必须完全一致,才证明令牌未被篡改。
- 业务验证:签名验证通过后,再使用载荷中的信息(如user、role)进行业务逻辑处理。
步骤3:漏洞成因分析
“签名验证缺失”漏洞的发生,核心在于第4步被完全跳过或错误实现。以下是几种常见的错误实现场景:
- 完全跳过验证:服务器解码JWT的载荷后,直接使用其中的数据,没有进行任何签名校验。攻击者可以任意构造JWT,修改载荷内容。
- 使用错误的验证库/配置:某些JWT库(如Node.js的
jsonwebtoken的早期版本或某些用法)默认不验证签名,需要开发者显式调用verify函数。如果开发者错误地使用了仅解码的decode函数,就会导致漏洞。 - 支持
none算法:JWT规范允许签名算法为none,表示不进行签名。如果服务器配置不当,没有在允许的算法列表中显式排除none,攻击者可以将头部改为{"alg":"none","typ":"JWT"},并将签名部分置空,服务器可能会接受此无签名的令牌。注意:现代标准库通常已禁止此算法,但仍需警惕。 - 密钥混淆/弱密钥:虽然这属于另一种攻击(Key Confusion),但有时会与验证缺失混淆。这里的“缺失”特指验证步骤本身不存在。
步骤4:漏洞利用实战演示
假设一个应用存在此漏洞,其登录后返回的JWT格式如下,用于标识用户身份和角色:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciIsImV4cCI6MTY3MjUwMDAwMH0.xxxxxx
攻击者(Attacker)的目标是:将自己伪装成管理员用户(admin)。
攻击步骤:
- 截获或获取一个有效JWT:攻击者可以通过注册普通账号,或通过其他信息泄露方式获得一个合法JWT。
- 解码并分析JWT:
- 将第一部分解码,得到头部:
{"alg":"HS256","typ":"JWT"}。 - 将第二部分解码,得到载荷:
{"user":"alice","role":"user","exp":1672500000}。
- 将第一部分解码,得到头部:
- 篡改载荷:
- 将载荷中的
"user"字段修改为"admin",将"role"字段修改为"administrator"。同时,为了避免令牌过期,可以修改"exp"为一个未来的时间戳。 - 修改后的载荷变为:
{"user":"admin","role":"administrator","exp":1900000000}。
- 将载荷中的
- 重新编码:将原始头部(无需修改
alg字段,因为服务器不验证)和篡改后的载荷分别进行Base64Url编码。 - 构造恶意JWT:由于服务器不验证签名,攻击者不需要知道签名密钥,也不需要生成有效的签名。他可以简单地拼接编码后的头部、载荷和一个空的或任意的第三部分。
- 恶意JWT示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImV4cCI6MTkwMDAwMDAwMH0.(注意最后签名部分为空)。 - 或者保留原始签名,但服务器不验证,所以任何签名都可以。
- 恶意JWT示例:
- 发送请求:将构造好的恶意JWT放入HTTP请求的
Authorization头部(例如:Bearer <恶意JWT>),发送给受保护的API端点。 - 结果:漏洞服务器解码JWT,看到载荷中有
"user":"admin"和"role":"administrator",由于跳过了签名验证,它直接信任了这些数据,从而授予攻击者管理员权限。
步骤5:漏洞防御措施
防御的核心是强制、正确地执行签名验证。
- 使用安全的标准库:使用成熟、维护活跃的JWT库(如Java的
jjwt, Python的PyJWT, Node.js的jsonwebtoken, Go的github.com/golang-jwt/jwt),并遵循其安全最佳实践。 - 始终调用验证函数:
- 错误做法:
payload = jwt.decode(token, options={“verify_signature”: False})(这是显式关闭验证)。 - 正确做法:
payload = jwt.verify(token, ‘your-256-bit-secret’, algorithms=[“HS256”])(必须提供密钥和允许的算法列表)。
- 错误做法:
- 显式指定允许的算法:在验证时,明确指定服务器所接受的算法列表(如
algorithms=[“HS256”]),绝不使用允许所有算法的通配符(如algorithms=None或*),这能有效阻止none算法攻击。 - 使用强密钥并安全管理:使用足够长度和随机性的密钥(如HS256至少256位随机字节),并将密钥安全地存储在环境变量或密钥管理服务中,而非硬编码在代码里。
- 验证所有必要声明:签名验证通过后,仍需验证令牌的有效期、颁发者、受众等业务声明。
- 依赖框架中间件:在使用Spring Security、Passport.js等框架时,使用官方或社区认可的、经过安全审计的JWT中间件,并仔细配置。
总结:JWT签名验证缺失攻击的本质是服务器逻辑上省略了最重要的安全步骤。防御的关键在于在代码中强制、显式地进行签名验证,并使用安全的库和配置。在任何涉及JWT的身份验证和授权逻辑中,签名验证都必须是第一个且不可绕过的检查点。