Part 01、 什么是JWT?
在JWT官网的基本概念中,JSON Web Token (JWT)定义了一个在各方之间通过 JSON 对象安全传输信息的方式,它将用户信息加密保存在JSON格式的token中,服务端将不会保存任何与用户认证的相关配置信息,只保存校验token签名的密钥,通过签名的校验来判断用户身份是否合法。此时,这种基于token的身份验证方法能够代替传统的cookie+session身份验证方法。
1.1 传统的cookie+session身份验证方法认证流程
- 用户端在浏览器中输入用户名和密码,发送到服务器。
- 服务器通过密码校验后生成session并保存到数据库中。
- 为用户端生成一个sessionid,并在浏览器中存放拥有sessionid的cookie,当再次发出请求时均会有对这个cookie信息的访问。
- 服务器获取用户cookie,根据其中的sessionid在数据库中查找,进而判断请求是否有效。
- 这种session+cookie的模式,是互联网应用中最为流行的用户认证模式,但是这种模式的扩展性并不好,如何解决这个问题呢?接下来让我们继续认识扩展性更好的JWT认证。
1.2 基于JWT的认证流程
JWT保存在客户端,服务器端只需要通过密钥解密技术对JWT进行分析和验证,不需要再繁琐地进行数据库查询和管理。也就是说服务器不用负担保存任何保存session数据的任务,减轻服务器压力,因此比较容易实现扩展。
JWT认证流程:
- 用户端在浏览器中输入用户名和密码,服务器通过密码校验后生成JWT。
- 客户端获取到token,存储到cookie或local storage中,当再次发出请求时均会有对这个token信息的访问。
- 用户在request请求头中包含JWT,并发给服务器。
- 服务端将会检查请求头中的 JWT 信息,如果正确,则返回响应并允许用户操作。
Part 02、 JWT格式
物联网安全是JWT由三部分组成,中间用点(.)分隔,依次如下:Header(头部)、Payload(负载)、Signature(签名)。JWT格式如下图2所示。
2.1 Header(头部)
JWT的头部其实就是个JSON对象,它承载着两部分信息:
- 声明类型
- 声明加密的算法
将头部信息通过Base64URL加密转成字符串即为JWT头部。
2.2 Payload(负载)
负载就是存放有效信息的地方,这些有效信息包含三个部分:
- 标准中注册的声明
- 公共的声明
- 私有的声明
同样将上面的JSON对象使用Base64URL加密转成字符串即为JWT负载。
2.3 Signature(签名)
服务器通常通过散列header和payload来生成签名。在一些情况下,还会加密生成的哈希值,无论是否加密,过程中都会涉及到签名密钥。服务器通过读取header中指定的签名算法,按照下表中的公式产生签名。
签名机制为服务器提供防篡改的验证方法:
- 因为签名是直接从令牌的前两部分派生的,因此更改header或payload的单个字节,都会导致签名失配。
- 在不知道服务器的签名密钥的情况下,不能为已经给定的header或payload生成正确的签名。
JWT三个组成部分的解码展示如下图3所示。
Part 03、 什么是JWT攻击?
3.1 JWT攻击
JWT攻击是指攻击者向服务器发送篡改过的JWT,实施恶意操作的行为。一般情况下,攻击的目的是伪造身份认证的用户,来绕过身份验证和访问控制的阻拦。而如果攻击者能够生成任意的有效令牌,就能够提升用户权限或假冒其他合法用户的身份,从而对这些伪造的用户账户实施完全的接管。
3.2 攻击原理
JSON Web Token(JWT)存在的漏洞往往源于应用程序对JWT的处理存在缺陷,进而影响了其安全性。JWT及其相关规范的设计赋予了网站开发人员较大的自由度,允许自主决定许多实现细节,但是这种自由度也可能导致安全问题的出现。
这些实现缺陷往往直接关联于JWT签名的验证,即便进行了严格的签名验证,攻击者仍有可能通过篡改JWT负载中的数据,传递恶意值给应用程序,从而干扰正常业务流程。此外,关于传递签名的可信性,主要取决于密钥的安全性,但是如果服务器的密钥存在泄漏或被破解,攻击者将能够生成合法的签名,进而伪造令牌并严重威胁整个身份验证与授权机制的完整性。
Part 04、 JWT常见安全漏洞
了解了JWT攻击原理,再让我们继续了解一下JWT相关常见漏洞,主要有以下类型:
1.Accepting arbitrary signatures -- 接受任意签名
2.Accepting tokens with no signature -- 没有签名的令牌
3.Brute-forcing secret keys -- 暴力破解密钥
4.JWT header parameter injections -- JWT 头参数注入
- Injecting self-signed JWTs via the jwk parameter --jwk参数注入自签名
- Injecting self-signed JWTs via the kid parameter -- kid参数注入自签名
- Injecting self-signed JWTs via the jku parameter --jku参数注入自签名
4.1 Accepting arbitrary signatures -- 接受任意签名
JWT库通常会提供一个验证token的方法,同时也会提供对其解码的方法。例如Node.js库jsonwebtoken的两个方法:verify()、decode()。如果开发只把decode()方法传给token而没有传递verify()方法,这意味着攻击者可以将随意修改payload后的JWT发送给服务器,服务器仅仅会对JWT进行解码解析却没有验证签名是否正确,从而造成伪造用户、越权访问等安全问题。
4.2 Accepting tokens with no signature -- 没有签名的令牌
JWT头部有一个重要参数“alg”,它会指明JWT签名所采用的算法。JWT支持多种算法来进行签名,也可以不进行签名。通过修改alg参数为“none”,此时签名为空,这就是生成了“不安全的JWT”。
4.3 Brute-forcing secret keys -- 暴力破解密钥
某些签名算法,会使用一个字符串作为密钥,而当我们知道JWT签名算法的时候,就必须要保证这个密钥不能被脚本轻易猜测或暴力破解,否则攻击者可以用任意header和payload来签名创建合法JWT。
4.4 JWT header parameter injections -- JWT头参数注入
根据JWS规范,只有头部参数alg是一定要有的。但是在实际生产中,JWT头部不仅仅有alg参数,往往还包含其它参数。以下三个是实际生活中通常测试攻击的参数:
➤ jwk(JSON Web Key):是一种用于表示密钥的嵌入式 JSON 对象。正常时候服务器应该要对其进行限制,只能使用特定的公钥白名单进行签名验证。然而,如果出现配置失误或疏忽,某些服务器可能会误用JWK参数内嵌的任何密钥进行签名验证操作。这就意味着,如果出现jwk配置失误,攻击者就可以通过RSA私钥对已经修改过的payload进行签名,并将RSA公钥嵌入到JWK头部,从而绕过签名认证。
➤ kid(Key ID):即密钥标识符,被用作提供一个独特的标识,当存在有多个密钥的情况下,服务器就会通过此ID来准确识别应用于验证的正确密钥。但是,系统并不具备辨别用户意图的能力,因此如果缺乏对参数的过滤措施,攻击者就可能篡改密钥文件,从而实现任意文件读取攻击,借此读取系统内的各类文件资源。
➤ jku(JSON Web Key Set URL):是一种用于提供URL的机制,发送jwk的地址。与"key ID" (kid) 类似,jku 也可以由用户根据特定输入数据来指定。一旦用户的输入数据没有经过严格的过滤和验证,就可能导致潜在的安全漏洞。
上述用户可操控的参数用于指示接收方服务器在进行签名验证时所需的密钥信息,如果攻击者修改这些可控参数可能会导致任意文件读取、SQL注入等安全问题。
Part 05、 JWT安全防护
根据上述安全漏洞,总结有以下防护措施:
1.不要在token中设置任何敏感信息,同时不要将payload中的时间戳失效设定太长,防止被盗的token长时间被滥用。
2.使用最新的库来处理JWT,并确保完全理解它的工作原理以及任何安全隐患。
3.在服务器端,限制JWT头部中“alg”参数的值只能使用特定的、安全的算法。禁止使用不安全的算法,如"none"算法。
4.针对JWT头部进行严格的白名单设置,只允许特定的参数值,拒绝任何不在白名单内的参数值。
Part 06、 总结
已签名的JWT具有标头、负载和签名三部分,每个都在确保JWT可用于安全地存储和传输关键信息方面发挥着至关重要的身份验证作用。了解这三个组件对于正确使用JWT至关重要,同时由于签名的重要性,我们有必要对JWT安全进行了解并做好防护,从而做到防范于未然。