JWT的安全问题
JWT介绍
JWT的定义
Json web token (简称JWT),是目前最流行的跨域认证解决方案,是一种认证授权机制。
JWT 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的由来
- HTTP协议本身是无连接、无状态的。而这对于单纯的浏览型网页是十分友好的,它不需要记住是谁发送的请求,每一个请求对于它来说都是全新的,服务器也不需要额外的资源去记忆没有个用户。
- 但是我们现在面临的网站大部分都需要管理不同的用户,他们有不同的身份,例如游客、普通用户、管理员、超级管理员等等。而这时我们就需要管理会话,需要认识每一个用户,知道他们的具体信息。这时有人就想出了一个办法,那就是给大家都发一个会话标识(session id),这个会话标识是一串很长的随机字符串,每个人都不一样, 然后每次大家发起HTTP请求的时候,把这个字符串给发送给服务器, 这样服务器就能区分开谁是谁了。
- 不过这种方法虽然解决了基本的认证问题,但是却给服务器带来了巨大的负担。每个人只需要保存自己的session id,而服务器要保存所有人的session id, 如果访问服务器的用户多了, 甚至需要保存成千上万,甚至几十万个用户的数据,这对服务器说是一个巨大的开销 ,而且 严重的限制了服务器扩展能力。比如说我用两个机器组成了一个集群, 用户小明通过机器A登录了系统, 那session id会保存在机器A上, 假设小明的下一次请求被转发到机器B怎么办? 机器B可没有小明的 session id啊。这时有人提议将所有的session id保存到一个地方,让所有的服务器到这个机器中去取数据,这样一来,就不用复制了。但是这种方法也有很大的隐患,如果那个负责session 的机器挂了, 所有人就都得重新登录一遍。这种情况有人尝试把这个单点的机器也搞出集群,增加可靠性, 但不管如何, 这小小的session 对服务器来说是一个沉重的负担
- 于是有人就思考, 我为什么要保存这可恶的session呢, 只让每个客户端去保存该多好。是了,我为什么要保存session——是为了确认每一个用户的身份,并防止他们伪造。那么我可以将用户的身份信息用签名来保证它的不可篡改性,然后让用户自己保存数据。比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥, 对数据做一个签名, 把这个签名和数据一起作为token , 由于密钥别人不知道, 就无法伪造token了。当用户把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道用户已经登录过了,并且可以直接取到用户的user id , 如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,没有认证。
- 而顺着这个思路,有人就想让用户保存更多的信息,这样服务器的负担就更小了。有人提出将用户的一些其它数据和user id一起发送给用户,让用户自己保存,然后利用密钥保证安全性,这就是JWT。JWT将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。JWT的出现进一步释放了服务器的性能。
JWT的构成
实际的 JWT 大概就像下面这样
它是一个很长的字符串,中间用点(.)分隔成三个部分。JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
Header(头部)
Payload(负载)
Signature(签名)
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg":"HS256",
"typ":"JWT"
}
jwt的头部承载两部分信息:
- 声明类型:typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
- 声明加密的算法:通常直接使用 HMAC SHA256(写成 HS256)
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Base64URL
Base64URL和Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
Payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64url加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT和Token的区别
相同点:
- 都是访问资源的令牌
- 都可以记录用户的信息
- 都是使服务端无状态化
- 都是只有验证成功后,客户端才能访问服务端上受保护的资源
不同点:
-
Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
-
JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
普通token需要后端存储与用户的对应关系,而JWT自身携带对应关系。在大型的多系统中,普通token每次请求需要向用户资源服务器获取对应用户信息同时验证token,而JWT只需要验证签名有效即可信任且JWT自带用户信息, 无需额外请求。
JWT使用方式
一般是在请求头里加入Authorization
,并加上Bearer
标注:
Authorization: Bearer + <token>
这是一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。 服务器受保护的路由将在授权头中检查有效的JWT,如果存在,则允许用户访问受保护的资源。 由于JWT是独立的,所有必要的信息都在那里,减少了多次查询数据库的需求。
这使得我们可以完全依赖无状态的数据API,甚至向下游服务提出请求。 无论哪些域正在为API提供服务并不重要,因此不会出现跨域资源共享(CORS)的问题,因为它不使用Cookie
JWT的认证流程:
- 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
- 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
- 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT。
- 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
JWT的安全风险
1.敏感信息泄露
我们能够轻松解码payload和header,因为这两个都只经过Base64Url编码,而有的时候开发者会误将敏感信息存在payload中。
一般我们遇到jwt字符串可以到https://jwt.io/这个网站解密。
2.未校验签名
某些服务端并未校验JWT签名,所以,可以尝试修改signature后(或者直接删除signature)看其是否还有效。
3.签名算法可被修改为none(CVE-2015-2951)
头部用来声明token的类型和签名用的算法等,比如:
{
"alg": "HS256",
"typ": "JWT"
}
以上header指定了签名算法为HS256,意味着服务端利用此算法将header和payload进行加密,形成signature,同时接收到token时,也会利用此算法对signature进行签名验证。
但是如果我们修改了签名算法会怎么样?比如将header修改为:
{
"alg": "none",
"typ": "JWT"
}
那么服务端接收到token后会将其认定为无加密算法, 于是对signature的检验也就失效了,那么我们就可以随意修改payload部分伪造token。
https://jwt.io将alg为none视为恶意行为,所以,无法通过在线工具生成JWT,可以用python的jwt库来实现:
import jwt
jwt.encode({"user":"admin","action":"upload"},algorithm="none",key="")
用none算法生成的JWT只有两部分了,根本连签名都不存在。
4.签名密钥可被爆破
jwt使用算法对header和payload进行加密,如果我们可以爆破出加密密钥,那么也就可以随意修改token了。
JWT爆破脚本:https://github.com/Ch1ngg/JWTPyCrack
也可以使用下面的脚本爆破
jwt_str = "xxx.ttt.zzz"
path = "D:/keys.txt"
alg = "HS256"
with open(path,encoding='utf-8') as f:
for line in f:
key_ = line.strip()
try:
jwt.decode(jwt_str,verify=True,key=key_,algorithm=alg)
print('found key! --> ' + key_)
break
except(jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError):
print('found key! --> ' + key_)
break
except(jwt.exceptions.InvalidSignatureError):
continue
else:
print("key not found!")
5.修改非对称密码算法为对称密码算法(CVE-2016-10555)
JWT的签名加密算法有两种,对称加密算法和非对称加密算法。
对称加密算法比如HS256,加解密使用同一个密钥,保存在后端。
非对称加密算法比如RS256,后端加密使用私钥,前端解密使用公钥,公钥是我们可以获取到的。
如果我们修改header,将算法从RS256更改为HS256,后端代码会使用RS256的公钥作为HS256算法的密钥。于是我们就可以用RS256的公钥伪造数据
比如说这道CTF题目:https://skysec.top/2018/05/19/2
6.伪造密钥(CVE-2018-0114)
jwk是header里的一个参数,用于指出密钥,存在被伪造的风险。比如CVE-2018-0114: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0114
攻击者可以通过以下方法来伪造JWT:删除原始签名,向标头添加新的公钥,然后使用与该公钥关联的私钥进行签名。
比如:
{
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"kid": "TEST",
"use": "sig",
"e": "AQAB",
"n": "oUGnPChFQAN1xdA1_f_FWZdFAis64o5hdVyFm4vVFBzTIEdYmZZ3hJHsWi5b_m_tjsgjhCZZnPOLn-ZVYs7pce__rDsRw9gfKGCVzvGYvPY1hkIENNeBfSaQlBhOhaRxA85rBkg8BX7zfMRQJ0fMG3EAZhYbr3LDtygwSXi66CCk4zfFNQfOQEF-Tgv1kgdTFJW-r3AKSQayER8kF3xfMuI7-VkKz-yyLDZgITyW2VWmjsvdQTvQflapS1_k9IeTjzxuKCMvAl8v_TFj2bnU5bDJBEhqisdb2BRHMgzzEBX43jc-IHZGSHY2KA39Tr42DVv7gS--2tyh8JluonjpdQ"
}
}
JWT tool
此工具可用于测试jwt的安全性,地址是 https://github.com/ticarpi/jwt_tool
示例用法:
JWT的使用建议
- 不要存放敏感信息在Token里。
- Payload中的exp时效不要设定太长。
- 开启Only Http预防XSS攻击。
- 如果担心重播攻击(replay attacks )可以增加jti(JWT ID),exp(有效时间) Claim。
- 在你的应用程序应用层中增加黑名单机制,必要的时候可以进行Block做阻挡(这是针对掉令牌被第三方使用窃取的手动防御)。