JWT 简介
JWT是JSON Web Token 的简称,根据https://www.rfc-editor.org/rfc/rfc7519的定义如下:
A string representing a set of claims as a JSON object that is encoded in a JWS or JWE, enabling the claims to be digitally signed or MACed and/or encrypted.
翻译过来大概意思就是:将一组声明表示为通过JWS或者JWE编码的JSON对象的字符串,使声明能够被数字签名或加密方式保护。
总结成一句话就是,它是一串经过数字签名或者加密保护的JSON对象格式的字符串。
下面个是几个关于JWT的概念:
JWT Claims Set:一个包含JWT传递的声明JSON对象。
Claim:关于主题所断言的一条信息。Claim表示为由声明名称和声明值组成的名称/值对。
Claim Name:Claim 声明的名称部分。Claim 名称总是一个字符串。
Claim Value:Claim 声明的值。Claim 值可以是任何JSON值,也就是说它可以是字符串,也可以是一个JSON对象。
通常一个JWT有三个部分组成,他们是头部(Header)、载荷(Payload)和签名(Signature),组成的格式如下:
BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload) || '.' || BASE64URL(JWS Signature)
Header:声明编码的对象是JWT的类型以及使用的算法,示例如下:
{"typ":"JWT",
"alg":"HS256"}
通常JWT使用Base64url的编码方式,上述的JSON Header经过编码之后,变成:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
Payload:上述的JWT Claims Set的部分是JWT的payload,也是经过Base64url进行编码的,例如:
{"username":"test",
"exp":1300819380,
"role":admin}
经过编码的值为:eyJ1c2VybmFtZSI6InRlc3QiLAoiZXhwIjoxMzAwODE5MzgwLAoicm9sZSI6YWRtaW59。
Signature:就是使用Header所描述的算法对Base64url编码之后的Header和Payload进行签名之后的值。JWT Signature称为JWS,有一个专门的标准https://www.rfc-editor.org/rfc/rfc7515。如果使用加密的算法,就称为JWE,由另外一个标准https://www.rfc-editor.org/rfc/rfc7516。根据标准RFC7515可以知道Signature的计算公式是:
HMAC256(ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload))),假设得到的Signature:WcPGXClpKD7Bc1C0CCDA1060E2GGlTfamrd8-W0ghBE。
根据JWT的生成公式,就可以得到最终的JWT是:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.
eyJ1c2VybmFtZSI6InRlc3QiLAoiZXhwIjoxMzAwODE5MzgwLAoicm9sZSI6YWRtaW59.
WcPGXClpKD7Bc1C0CCDA1060E2GGlTfamrd8-W0ghBE
JWT 的作用
通常当以用户登录之后,应用程序管理用户会话的方法是使用Cookie,将SessionID写入Cookie,当用户连接到应用程序时,它生成一个唯一的会话标识符,该标识符存储在服务器上,然后用“Set-Cookie”头返回给客户端。这样,会话cookie就存储在web浏览器里。cookie被设计成在每次请求时由浏览器自动地发送回服务器。该解决方案是处理普通用户登录和注销场景的好方法。但是随着微服务的发展和API的兴起,这种方式也越来越不能适应新的需求。因为这种机制不允许多个平台或应用程序轻松地使用单个会话对用户进行身份验证。此外,服务器也需要在其内存中保存会话的状态和数据。
JWT令牌是“无状态的”,这意味着会话信息不存储在服务器端。除了在服务器上节省大量内存之外,JWT令牌还可用于对多个应用程序中的用户进行身份验证。为此,不同的应用程序需要共享相同的私钥来签署和验证令牌。因此,用户将能够在管理用户帐户的应用程序上进行一次身份验证,并无缝地使用相同私钥的其他应用程序来验证令牌的有效性。
一个典型的JWT使用流程如下:
JWT token(JSON Web Token)的主要作用如下:
- 授权认证:在用户登录成功后,服务端会提供一个JWT token给客户端。这个token是经过签名认证的,每次客户端带着这个token访问服务端接口时,服务端都会进行验证。这样,客户端就拥有了访问服务端的权利。
- 单点登录:JWT的开销较小,且可以在不同域中使用,因此它常被用于单点登录。通过使用JWT,用户可以在多个系统之间进行无缝登录,提高用户体验。
在生成JWT token时,通常会包含用户的某些信息,如用户ID或邮箱等,但一般不会包含密码。这种设计是为了避免用户密码的泄露。如果JWT被别人劫持,其有效期通常是较短的,必须重新生成一个新的token。同时,为了保护token的安全,通常会将其存储在cookie中,而不是直接存储在账号信息中。
JWT 的存储
JWT可以被存储在Cookie里,也可以存储在浏览器的sessionStorage 或者localStorage 里,每种方法都有各自的利弊。
sessionStorage 或者localStorage
下图是一个将JWT存储在sessionStorage的例子:
如果存储在sessionStorage 或者localStorage 里,最大的不方便之处就是,在每次发送请求时,都需要添加一个Header,例如:Authorization: Bearer <token>。
如果应用程序是使用JavaScript基于Ajax写的,这么使用效果会非常好,而且可以有效地预防CSRF的问题。但是问题是JWT需要可以通过JavaScript访问,这就给了XSS攻击机会。
Cookie
JWT存储在Cookie最大的好处就是每次发送请求就不需要关注Token的问题,浏览器会自动管理,但是,这也给XSS攻击带来了可乘之机。因此,需要在Cookie里加上httponly标志和Secure标志,防止JWT泄露。放在Cookie里最大的缺点是不能预防CSRF,因为浏览器会自动将Cookie发送给后台服务。
JWT 的安全问题以及解决方案
问题一、接受任意签名
JWT库通常提供一种方法来验证令牌,另一种方法只对它们进行解码。例如,Node.js库jsonwebtoken有verify()和decode()。有时,开发人员会混淆这两个方法,只将传入的令牌传递给decode()方法,decode成功就认为令牌验证成功了。这实际上意味着应用程序根本不验证签名,导致签名的认证被绕过。
解决方案: 认真阅读API的功能说明,了解如何正确使用API,按照正确的方法和顺序调用API,并在使用之前充分验证。
问题二、JWT使用None算法
在JWT的Header里有一个alg用于专门指定使用什么签名算法,这么做的目的是使得JWT实现起来更灵活,解耦生成JWT的代码和验证JWT的代码。Header的内容如下:
{"typ":"JWT",
"alg":"none"}
alg声明中none值的特殊情况告诉客户端和资源服务器,实际上根本没有对JWS进行签名。不建议使用此选项,如果希望启用未签名的JWT,应该绝对确定自己在做什么,使用它的目的是什么,以及是否能够达到目的。
由于alg的值是可以自由控制的,攻击者可以尝试将alg的算法修改为none,【即使JWT是没有签名的,编码之后的令牌也需要以.结尾。】。验证token的一端如果没有对算法进行严格验证,直接获取算法并且使用就可以绕过签名的认证。使用none导致的漏洞,如:CVE-2018-1000531。
这种JWT是不安全的,由于存在明显的危险,服务器通常会拒绝没有签名的令牌。但是,由于这种过滤依赖于字符串解析,因此有时可以使用传统的混淆技术(如混合大写和意外编码)绕过这些过滤器。
解决方案: 指定使用安全的算法的列表,并且在使用时验证签名算法是否在列表中,安全的签名算法列表可以参考:https://jwt.io/#debugger-io。
问题三、JWT中含有敏感信息
JWT是Key-Value标记令牌,这意味着它包含了一些数据,不管怎么样,它都在令牌里,很容易获得。这是否是个问题取决于令牌的目标受众。ID令牌用于客户端的开发人员,希望对它进行解码,并让客户机使用它的数据。另一方面,访问令牌是为API开发人员设计的。API应该解码并验证令牌。如果向客户端发出JWT访问令牌,必须记住,客户端开发人员将能够访问该令牌内的数据。
由于所有人都可以读取令牌的内容,如果JWT中含有敏感的个人信息,如PII信息,这就意味着所有能够得到JWT的人,都可以解码JWT并且获取其中的数据。这些信息会随着JWT在各个服务之间传递,增加了泄露的可能。不仅仅是个人信息,可能在JWT中泄露,更重要的是关于API或者后台服务的任何有价值的信息都尽量避免放在JWT中,因为这些信息可能被攻击者收集起来攻击系统。
同时JWT只负责通过Signature验证完整性,对JWT里的信息一般不负责保密性,因此想在JWT添加Claim时,最好确认是否必要,对于没有必要放在JWT的信息尽量移除,避免不必要的麻烦。
尽管加密听起来像是保持数据私密性的优秀解决方案,但现实情况是,很难配置和维护安全的加密机制。此外,加密需要使用更多的计算资源,这可能会成为高流量应用程序的负担。
解决方案: 非必要的情况下,禁止在JWT中添加敏感信息。如果由于业务需要必须要传递一些敏感信息,可以尝试使用Opaque Token【例如:Phantom Token Approach,https://curity.io/resources/learn/phantom-token-pattern/;或者,Split Token Approach,https://curity.io/resources/learn/split-token-pattern/】。
问题四、JWT算法篡改
有些库在实现上有缺陷,虽然验证了不能使用none,但是,针对某个JWT的算法验证不严格,导致算法没有被验证是否篡改,这就可能导致一个严重的问题就是签名被绕过。
有时即使使用的比较安全的算法,如果代码实现上有逻辑问题,也会导致Signature的验证被绕过。
JWT规范还定义了许多非对称签名算法(基于RSA和ECDSA)。使用这些算法,令牌是使用私钥创建和签名的,但使用相应的公钥进行验证。这非常简洁:如果您发布了公钥,但将私钥留给自己,那么只有你可以签署令牌,但任何人都可以检查给定令牌是否正确签名。大多数JWT库都有这样的API,使用如下:
verify(string token, string verificationKey)
在使用HMAC签名的系统中,verificationKey将是服务器的秘密签名密钥(因为HMAC使用相同的密钥进行签名和验证):
verify(clientToken, serverHMACSecretKey)
对于使用异步加密算法验证时,使用的是公钥,验证代码如下:
verify(clientToken, serverRSAPublicKey)
不幸的是,攻击者可以常会利用这一点发起攻击。如果服务器期望得到一个用RSA签名的令牌,但实际上收到了一个用HMAC签名的令牌,那么它将认为公钥实际上是一个HMAC密钥。但是对于HMAC而言,私钥应该是保密的,而RSA的公钥确实公开的。这样攻击者就可以通过篡改签名算法把RSA的公钥当做HMAC的私钥,绕过签名的认证。【详细的介绍可以参考:https://portswigger.net/web-security/jwt/algorithm-confusion】
解决方案:接受JWT的服务端应该知道使用什么加密算法,从接收到的JWT中摘取算法的就会给攻击者篡改的机会。当然这是有些JWT的实现库有问题造成的,因此,在使用一些开源的库之前,最好是对库进行充分地测试和安全审计,确保签名的验证是准确无误地。
选择安全的JWT库,可以参考https://jwt.io/libraries,该网站提供了各种语言的JWT的库,尽量选择版本号高的带绿色标签的版本。
无论令牌是签名的(JWS)还是加密的(JWE),它都会在报头中包含一个声明。它指示用于签名或加密的算法。在验证或解密令牌时,应该始终使用系统接受的算法白名单检查此声明的值。这减少了攻击向量,即有人会篡改令牌,并迫使您使用不同的(可能不太安全的)算法来验证签名或解密令牌,而且白名单算法优于黑名单算法。
可以参考【https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms】查看所有支持的签名或者加密算法,并且针对每种加密算法给出了使用的建议。建议使用Recommended+和Recommended的算法。对于Prohibited的算法,应该避免使用。
更多详细信息也可以去阅读RFC7518。
在考虑签名时,基于椭圆曲线的算法被认为更安全。安全性和性能最好的选项是EdDSA虽然ES256(使用P-256和SHA-256的椭圆曲线数字签名算法ECDSA)也是一个不错的选择。大多数技术栈支持的最广泛使用的选项是RS256 (RSASSA-PKCS1-v1_5使用SHA-256)。前者比后者快得多,这是强烈推荐的主要原因之一。后者存在的时间更长,并且在不同的语言和实现中提供了更好的支持。
尽管不建议使用对称密钥,如果确实需要使用对称密钥,那么HS256(使用SHA-256的HMAC)应该是不错的选择。
问题五、JWT使用Hardcoded Key
在大量的安全审计过程中,发现JWT实现的一个普遍的问题就是密钥的管理,有很多应用为了编码的方便,直接将密钥硬编码写在代码里。例如,使用HMAC时,需要JWT的生产者和消费者都共享一个密钥,生产者用密钥生成签名,消费者用密钥验证签名。如果只是双方,还相对容易管理一些。如果后台是很多微服务,时间长了,可能都无法统计该密钥在哪些服务中在使用,管理就会很麻烦。最关键的是,一旦出现密钥泄露的情况,无法及时替换现有的密钥,因为没办法在第一时间找到哪些服务在使用。
解决方案:让系统有一套动态管理的密钥系统,可以及时更换密钥,虽然实现起来比较复杂,代价也比较大。但是,为了保证系统的安全,还是值得的。对系统安全要求更高的可以考虑HSM系统。
问题六、Hardcoded的JWT
有的系统甚至将JWT令牌直接写在程序里或者配置文件里,这样的JWT令牌问题就更大了,首先,这个JWT令牌是没有过期时间的,可以永久有效;其次,这个JWT令牌和Hardcoded Key一样的结果,就是一旦泄露,很难及时更换。
这类问题比较容易使用SAST工具或者写脚本来探测到。由于JWT Signature的算法是以一个列表,而JWT Header的格式是固定的,因此,可以先计算出所有的机密算法对应的JWT的Header,例如,计算出算法none的头的值是eyJ0eXAiOiJKV1QiLAoiYWxnIjoiTm9uZSJ9,通过这些字符串特征,自定义SAST的规则专门扫描这些字符串,只要是匹配的字符串,就肯定是Hardcoded JWT。
解决方案:禁止使用hardcoded JWT,使用安全的加密算法和可以动态更新的密钥实时生成JWT,并且根据应用的需求设置一个合理的超时时间。
问题七、JWT不可以被撤销
JWT虽然用起来很方便,但是也有一定的缺点。
- 一个用户被锁住或者禁用了,按理说,这个应该不再能够继续使用这个系统,但是由于JWT没有过期,依然可以使用这个JWT访问系统的一些资源;
- 一个用户发现自己的账号可能被盗用了,就去修改密码,如果攻击者已经登录了,它使用之前的密码得到的JWT依然有效;
- 用户登出系统了,JWT由于没有过期,依然会有效,不能及时登出系统。
总之,在session中使用JWT不是一个好主意,最主要的就是如何在会话退出时及时销毁JWT。JWT如果不能主动撤销,使用JWT就会可能存在以上问题,因此,需要在设计系统时就考虑到如何更新JWT和撤销JWT。
此类问题的检测也很容易,使用Proxy工具拦截一些请求,例如:Burpsuite的Repeater功能,然后从页面登出系统,再使用Burpsuite的Repeater重新发送一次拦截的请求,就可以验证此类问题是否存在。
解决方案:及时销毁一个JWT就需要将JWT的信息和用户对象进行绑定,以及JWT的创建时间和有效时长,当需要销毁一个JWT时,只需要更新有效时长就可以了。也可以将被销毁的JWT建立一个黑名单,放在缓存里和存储在数据库里,只要是在黑名单里的就认为是被销毁JWT。
问题八、JWT密钥破解
Hashcat是一个离线破解HMAC的密钥的工具,安装可以参考https://hashcat.net/wiki/doku.php?id=frequently_asked_questions#how_do_i_install_hashcat。使用Hashcat的前提是必须有一个对目标服务器有效的JWT和一个JWT密钥的列表,【列表可以参考https://github.com/wallarm/jwt-secrets/blob/master/jwt.secrets.list】,然后执行以下命令:
hashcat -a 0 -m 16500 <jwt> <wordlist>
由于hashcat在攻击者自己的机器上本地运行,并且不依赖于向服务器发送请求,因此这个过程非常快,即使在使用巨大的单词列表时也是如此。一旦锁定了密钥,就可以使用它对任何JWT头和有效负载生成有效签名。如果密钥是非常弱的密钥,也可以使用暴力破解的方式制定一个规则让工具自动组合不同的字符猜测可能的密钥。关于如何编辑一个JWT,可以参考Burpsuite的JWT Editor扩展,可以参考:Working with JWTs in Burp Suite - PortSwigger
解决方案:使用密钥时,需要使用一定复杂度的密钥,建议使用和Hash值一样长或者更长的密钥会比较安全,同时,也需要密钥的动态更新,以防在密钥泄露的时候,可以及时更换密钥。
由于使用对称密钥的算法时,密钥的管理会比较困难,对于一些比较重要的系统,建议使用非对称加密的算法。私钥保存在本地用于签名,公钥分发给各个合作方,用于验证签名,方便密钥的管理。
问题九、JWT不安全传输
在访问一个网站时,如果浏览器上显示如下图标,就意味着这个网站没有启用安全协议,传输的内容就会被嗅探的可能,导致JWT被泄露。
可以说,最危险的应用程序登录相关漏洞之一是当web通信会话未加密时。SSL和较旧版本的TLS存在已知的中间人攻击漏洞,这已经够糟糕的了。但是,允许在根本没有加密的情况下登录核心业务应用程序、内容管理系统和其他企业系统,这表明缺乏应有的谨慎,是在自找麻烦。
下图是典型的使用的TLS,但是却使用了不安全的版本。
使用不安全的SSL、TLS协议版本可能会导致BEAST攻击、POODLE攻击以及HeartBeat攻击等等。
解决方案:建议强制使用TLS1.2,对于不安全的加密算法,一律都通过配置禁用。
问题十、JWT的超时时间超长
有的应用为了和内部的其他微服务通讯的方便,将JWT的超时时间设置的很长,甚至有的应用为了实现的方便直接生成一个JWT之后永久使用,也没有更新机制。这就和问题六类似的问题了。无论什么情况,都尽量避免使用超时过长的JWT,特别是对权限比较高的账号或者服务。
解决方案:根据APP的敏感级别和账号的级别设置不同的时长,例如:账号管理的应用或者服务,可以考虑设置时效只够进行一次操作的时间,类似一次性的令牌。
JWT 的总结
JWT主要是用作访问令牌或ID令牌,它的安全性是保证系统访问控制的前提。随着JWT被越来越广泛地使用,攻击者也增加了对JWT的关注,研究各种可能的攻击方式。重要的是要记住,JWT安全性在很大程度上取决于令牌的使用和验证方式。仅仅因为JWT包含加密签名,并不自动意味着它是安全的,或者您应该盲目地信任令牌。除非遵循良好的实践,否则系统的API可能容易受到网络攻击。
因此,正确地设计和实现JWT对系统至关重要。下面列除了一些Best Practice,希望对大家正确地使用JWT能够有所帮助。
- 不要相信输入的任何信息,包括JWT。选择安全的库生成和验证JWT,如果要自己实现,也要按照标准rfc7519来实现。选择安全的加密算法,禁用none。对于输入的加密算法也要验证是否在支持的加密算法的白名单列表中。
- 总是设置一个合理的超时时间。JWT是自包含的按值令牌,一旦发出并交付给接收方,就很难撤销它们。因此,您应该使用尽可能短的令牌到期时间——最多几分钟或几小时。应该避免将令牌的过期时间设置为几天或几个月。Claim中的exp适用于设置超时时间的,也可以通过nbf【not-before】设置在某个具体时间之后才能使用。JWT需要有一个安全的撤销机制,保证用户登出系统时能够及时销毁JWT。另外,尽量不要在session中使用JWT,因为很难撤销JWT,即使sessino销毁的情况下,JWT可能依然有效。
- 签名需要正确验证密钥或证书。可以通过几种不同的方式从授权服务器获得这些密钥或证书。您可以在通讯过程中从授权服务器获取密钥,并确保所有资源服务器都可以访问这些密钥。但是,当密钥或证书发生更改时,这会产生问题。这就是为什么总是使用端点并从授权服务器动态下载密钥或证书(根据服务器在缓存控制标头中返回的内容缓存响应)是一种很好的做法。这允许一个简单的密钥动态更新,而且这样的动态更新都不会影响使用者的任何实现。请记住,如果密钥或证书是在JWT的报头中发送的,应该始终检查它们是否属于预期的颁发者,例如,验证证书中的信任链。
- 尽量避免使用对称加密算法,当使用对称密钥时,所有各方都需要知道共享密钥。随着参与方数量的增加,保护秘密的安全变得越来越困难,一旦它被泄露,就很难替换它。如果出于某种原因,必须使用对称签名,请尝试使用临时秘密或者可以动态更新的密钥,这将有助于提高安全性。
- 总是检查发行者,当JWT中含有iss的Claim时,应该在本地有一个白名单列表,在使用JWT之前,检查iss是否在期望的白名单列表中。对于动态下载验证或解密令牌所需的密钥,那么这一点尤其重要。如果有人向您发送伪造的JWT,其中包含其发行者,然后您从该发行者下载密钥,那么应用程序将验证JWT并将其作为正版接受。同样当Header里有aud时也要严格检查aud的值,以防JWT的用途被误用。
- 安全标准和加密的安全级别可能会迅速变化,因此密切关注行业中正在发生的事情是很有必要的。有可能某种加密算法在某个时刻被宣布是不安全的,例如:DES、MD5和SHA1等。
- 更多的Best Practice,可以参考rfc8725。
The Hacker's Guide to JWT Security - Events | Microsoft Learn
JWT attacks | Web Security Academy
JWT tokens and security - working principles and use cases
JWT authentication: Best practices and when to use it - LogRocket Blog
JSON Web Token Introduction - jwt.io
JWT Token Security Best Practices | Curity
Critical vulnerabilities in JSON Web Token libraries