安全婴儿床:JWT



许多应用程序使用JSON Web令牌(JWT)来允许客户端标识自己,以便在身份验证后进行进一步的信息交换。

JSON Web令牌是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方式来安全地在各方之间作为JSON对象传输信息。


该信息经过了数字签名,因此经过验证和可靠。
可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。

JSON Web令牌用于传输有关客户端身份和特征的信息。 此“容器”由服务器签名,因此客户端不会对其进行干扰,也无法更改例如标识数据或任何特征(例如,从简单用户到管理员的角色或更改客户端的登录名)。

在成功认证的情况下创建此令牌,并在开始执行每个客户端请求之前由服务器检查该令牌。 该令牌由应用程序用作客户端的“身份证”(包含有关他的所有信息的容器)。 服务器具有以安全方式验证令牌的有效性和完整性的能力。 这使应用程序可以是无状态的(无状态应用程序不会保存在一个会话中生成的客户端数据供与该客户端进行下一会话使用(每个会话是独立的)),并且身份验证过程与所使用的服务无关(就客户端和服务器技术而言)可能会有所不同,甚至包括传输通道,尽管HTTP是最常用的)。

使用JWT的注意事项


即使JWT令牌易于使用,并允许您在没有状态(无状态)的情况下提供服务(主要是REST),该解决方案也不适用于所有应用程序,因为它带有一些警告,例如存储令牌的问题。

如果应用程序不必是完全无状态的,则可以考虑使用所有Web平台提供的传统会话系统。 但是,对于无状态应用程序,如果正确实现,则JWT是一个不错的选择。

JWT的问题和攻击


使用NONE哈希算法


当攻击者更改令牌并更改哈希算法(“ alg”字段)以通过none关键字指示令牌完整性已得到验证时,会发生类似的攻击。 一些库将使用none算法签名的令牌视为具有经过验证的签名的有效令牌,因此攻击者可以更改令牌的有效载荷,并且应用程序将信任该令牌。

为了防止攻击,您必须使用不受此漏洞影响的JWT库。 另外,在令牌验证期间,您必须明确请求使用期望的算法。

实施示例:

//  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //        //    HMAC-256 - JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build(); //   DecodedJWT decodedToken = verifier.verify(token); 

令牌拦截


当令牌已被攻击者拦截或窃取,并且攻击者使用令牌使用特定用户的凭据来访问系统时,就会发生攻击。

保护包括在令牌中添加“用户上下文”。 用户上下文将包含以下信息:

  1. 在认证阶段生成并包含在令牌中的随机字符串,并作为更安全的cookie发送到客户端(标志:HttpOnly + Secure + SameSite + cookie前缀)。
  2. 来自随机字符串的SHA256哈希将存储在令牌中,以便任何XSS问题都不会允许攻击者读取随机字符串的值并设置期望的cookie。

IP地址不会在上下文中使用,因为在某些情况下IP地址可能会在一个会话中更改,例如,当用户通过其手机访问应用程序时。 然后,IP地址不断合法地更改。 此外,使用IP地址可能会在符合欧洲GDPR的水平上引起问题。

如果在令牌验证期间接收到的令牌不包含正确的上下文,则必须将其拒绝。
实施示例:

成功认证后创建令牌的代码:

 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; //    private SecureRandom secureRandom = new SecureRandom(); ... //   ,     byte[] randomFgp = new byte[50]; secureRandom.nextBytes(randomFgp); String userFingerprint = DatatypeConverter.printHexBinary(randomFgp); //    cookie String fingerprintCookie = "__Secure-Fgp=" + userFingerprint + "; SameSite=Strict; HttpOnly; Secure"; response.addHeader("Set-Cookie", fingerprintCookie); // SHA256          // (  )  XSS      //     cookie MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      15     Calendar c = Calendar.getInstance(); Date now = c.getTime(); c.add(Calendar.MINUTE, 15); Date expirationDate = c.getTime(); Map<String, Object> headerClaims = new HashMap<>(); headerClaims.put("typ", "JWT"); String token = JWT.create().withSubject(login) .withExpiresAt(expirationDate) .withIssuer(this.issuerID) .withIssuedAt(now) .withNotBefore(now) .withClaim("userFingerprint", userFingerprintHash) .withHeader(headerClaims) .sign(Algorithm.HMAC256(this.keyHMAC)); 


验证令牌有效性的代码:
 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //     cookie String userFingerprint = null; if (request.getCookies() != null && request.getCookies().length > 0) { List<Cookie> cookies = Arrays.stream(request.getCookies()).collect(Collectors.toList()); Optional<Cookie> cookie = cookies.stream().filter(c -> "__Secure-Fgp" .equals(c.getName())).findFirst(); if (cookie.isPresent()) { userFingerprint = cookie.get().getValue(); } } //  SHA256      cookie  //       MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)) .withIssuer(issuerID) .withClaim("userFingerprint", userFingerprintHash) .build(); //   DecodedJWT decodedToken = verifier.verify(token); 

用户明确撤销令牌


由于令牌仅在令牌到期后才变得无效,因此用户没有内置功能可让您显式取消令牌。 因此,在盗窃的情况下,用户无法自己撤回令牌,然后阻止攻击者。

保护方法之一是引入令牌黑名单,该黑名单将适合于模拟传统会话系统中存在的“注销”功能。

具有取消日期的令牌的集合(以HEX的SHA-256编码)应超过已发行令牌的有效期,将被存储在黑名单中。

当用户想要“注销”时,他调用一项特殊服务,该服务将提供的用户令牌添加到黑名单中,这会导致该令牌立即被取消,以在应用程序中进一步使用。

实施示例:

黑名单存储库:
为了集中存储黑名单,将使用具有以下结构的数据库:

 create table if not exists revoked_token(jwt_token_digest varchar(255) primary key, revokation_date timestamp default now()); 

令牌撤销管理:

 //    (logout). //  ,      //         . public class TokenRevoker { //    @Resource("jdbc/storeDS") private DataSource storeDS; //      public boolean isTokenRevoked(String jwtInHex) throws Exception { boolean tokenIsPresent = false; if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //     try (Connection con = this.storeDS.getConnection()) { String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?"; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); try (ResultSet rSet = pStatement.executeQuery()) { tokenIsPresent = rSet.next(); } } } } return tokenIsPresent; } //    HEX      public void revokeToken(String jwtInHex) throws Exception { if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //             //   if (!this.isTokenRevoked(jwtInHex)) { try (Connection con = this.storeDS.getConnection()) { String query = "insert into revoked_token(jwt_token_digest) values(?)"; int insertedRecordCount; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); insertedRecordCount = pStatement.executeUpdate(); } if (insertedRecordCount != 1) { throw new IllegalStateException("Number of inserted record is invalid," + " 1 expected but is " + insertedRecordCount); } } } } } 

代币披露


当攻击者获得对令牌(或一组令牌)的访问权并提取存储在其中的信息(有关JWT令牌的信息使用base64编码)以获取有关系统的信息时,就会发生此攻击。 信息可以是例如安全角色,登录格式等。

保护方法非常明显,包括加密令牌。 使用加密分析保护加密数据免受攻击也很重要。 为了实现所有这些目标,使用了AES-GCM算法,该算法提供了具有关联数据的身份验证加密(AEAD)。 AEAD原语提供对称的身份验证加密功能。 保护此原语的实现,使其免受基于选定密文的自适应攻击。 加密纯文本时,可以选择指定必须经过身份验证但未加密的相关数据。

也就是说,用相关数据加密可以确保数据的真实性和完整性,但不能保证其保密性。

但是,应该注意,加密主要是为了隐藏内部信息而添加的,但是非常重要的一点是要记住,防止伪造JWT令牌的最初保护是签名,因此,应始终使用令牌的签名及其验证。

客户端令牌存储


如果应用程序存储令牌,从而发生以下一种或多种情况:

  • 令牌由浏览器自动发送(cookie存储);
  • 即使重新启动浏览器(使用浏览器localStorage容器),也会获得令牌;
  • 在XSS攻击的情况下获得令牌(cookie可用于JavaScript代码或令牌存储在localStorage或sessionStorage中)。

为了防止攻击:

  1. 使用sessionStorage容器将令牌存储在浏览器中。
  2. 使用Bearer方案将其添加到Authorization标头中。 标题应如下所示:

     Authorization: Bearer <token> 
  3. 将指纹信息添加到令牌。

通过将令牌存储在sessionStorage容器中,在XSS的情况下,它为盗窃提供了令牌。 但是,添加到令牌的指纹可以防止攻击者在其计算机上重用被盗的令牌。 要关闭攻击者的最大使用范围,请添加内容安全策略以限制执行上下文。

在某些情况下,攻击者会将用户的浏览上下文用作代理服务器,以通过合法用户使用目标应用程序,但是内容安全策略可以阻止与意外域的通信。

也可以实施身份验证服务,以便在安全cookie内发布令牌,但是在这种情况下,应实施针对CSRF的保护。

使用弱键创建令牌


如果在HMAC-SHA256算法中使用的用于签名令牌所必需的秘密很弱,则可以对其进行黑客攻击(使用蛮力攻击进行挑选)。 结果,攻击者可以根据签名伪造任意有效的令牌。

为避免此问题,您必须使用复杂的密钥:字母数字(大小写混合)+特殊字符。

由于只有计算机计算才需要密钥,因此密钥的大小可以超过50个位置。

例如:

 A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ<vcZTA3N-($>2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW' 

要评估用于令牌签名的秘密密钥的复杂性,可以结合JWT API对令牌应用密码字典攻击。

Source: https://habr.com/ru/post/zh-CN457090/


All Articles