-
Notifications
You must be signed in to change notification settings - Fork 44
/
VulnerabilityTest.java
161 lines (130 loc) · 7.99 KB
/
VulnerabilityTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*
* Copyright (c) 2018-2022, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.jwt;
import io.fusionauth.jwt.domain.JWT;
import io.fusionauth.jwt.ec.ECSigner;
import io.fusionauth.jwt.ec.ECVerifier;
import io.fusionauth.jwt.hmac.HMACSigner;
import io.fusionauth.jwt.hmac.HMACVerifier;
import io.fusionauth.jwt.rsa.RSAVerifier;
import org.testng.annotations.Test;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* @author Daniel DeGroff
*/
public class VulnerabilityTest extends BaseJWTTest {
@Test
public void test_SignedWithoutSignature() {
JWT inputJwt = new JWT()
.setSubject("123456789")
.setIssuedAt(ZonedDateTime.now(ZoneOffset.UTC))
.setExpiration(ZonedDateTime.now(ZoneOffset.UTC).plusHours(2));
String encodedJWT = JWT.getEncoder().encode(inputJwt, HMACSigner.newSHA256Signer("secret"));
String encodedJWTNoSignature = encodedJWT.substring(0, encodedJWT.lastIndexOf('.') + 1);
expectException(NoneNotAllowedException.class, () -> JWT.getDecoder().decode(encodedJWTNoSignature, HMACVerifier.newVerifier("secret")));
// Also cannot be decoded even if the caller calls decode w/out a signature because the header still indicates a signature algorithm.
expectException(MissingSignatureException.class, () -> JWT.getDecoder().decode(encodedJWTNoSignature));
}
@Test
public void test_ECDSA_CVE_2022_21449() {
// Note this test will always fail when run on Java 8, it will fail on Java 15, 16 and 17.
JWT inputJwt = new JWT()
.setSubject("123456789")
.setIssuedAt(ZonedDateTime.now(ZoneOffset.UTC))
.setExpiration(ZonedDateTime.now(ZoneOffset.UTC).plusHours(2));
// Sign it using ECDSA 256
for (String alg : Arrays.asList("256", "384", "521")) {
Signer signer = alg.equals("256")
? ECSigner.newSHA256Signer(readFile("ec_private_key_p_" + alg + ".pem"))
: alg.equals("384")
? ECSigner.newSHA384Signer(readFile("ec_private_key_p_" + alg + ".pem"))
: ECSigner.newSHA512Signer(readFile("ec_private_key_p_" + alg + ".pem"));
String encodedJWT = JWT.getEncoder().encode(inputJwt, signer);
String hackedEncodedJWT = encodedJWT.substring(0, encodedJWT.lastIndexOf('.') + 1) + Base64.getUrlEncoder().encodeToString(new byte[64]);
expectException(InvalidJWTSignatureException.class, () -> JWT.getDecoder().decode(hackedEncodedJWT, ECVerifier.newVerifier(readFile("ec_public_key_p_" + alg + ".pem"))));
}
}
@Test
public void test_encodedJwtWithSignatureRemoved() {
// Sign a JWT and then attempt to verify it using None.
JWT jwt = new JWT().setSubject("art");
String encodedJWT = JWT.getEncoder().encode(jwt, HMACSigner.newSHA256Signer("secret"));
String hackedJWT = encodedJWT.substring(0, encodedJWT.lastIndexOf('.'));
expectException(InvalidJWTException.class, () -> JWT.getDecoder().decode(hackedJWT, HMACVerifier.newVerifier("secret")));
}
@Test
public void test_noVerification() {
// Sign a JWT and then attempt to verify it using None.
JWT jwt = new JWT().setSubject("art");
String encodedJWT = JWT.getEncoder().encode(jwt, HMACSigner.newSHA256Signer("secret"));
expectException(MissingVerifierException.class, () -> JWT.getDecoder().decode(encodedJWT));
}
@Test
public void test_unsecuredJWT_validation() {
JWT jwt = new JWT().setSubject("123456789");
Signer signer = new UnsecuredSigner();
Verifier hmacVerifier = HMACVerifier.newVerifier("too many secrets");
String encodedUnsecuredJWT = JWT.getEncoder().encode(jwt, signer);
// Ensure that attempting to decode an un-secured JWT fails when we provide a verifier
expectException(NoneNotAllowedException.class, () -> JWT.getDecoder().decode(encodedUnsecuredJWT, hmacVerifier));
String encodedUnsecuredJWT_withKid = JWT.getEncoder().encode(jwt, signer, (header) -> header.set("kid", "abc"));
String encodedUnsecuredJWT_withoutKid = JWT.getEncoder().encode(jwt, signer);
Map<String, Verifier> verifierMap = new HashMap<>();
verifierMap.put(null, hmacVerifier);
verifierMap.put("abc", hmacVerifier);
// Ensure that attempting to decode an un-secured JWT fails when we provide a verifier with or without using a kid
expectException(NoneNotAllowedException.class, () -> JWT.getDecoder().decode(encodedUnsecuredJWT_withKid, verifierMap));
expectException(NoneNotAllowedException.class, () -> JWT.getDecoder().decode(encodedUnsecuredJWT_withoutKid, verifierMap));
}
@Test
public void test_vulnerability_HMAC_forgery() throws Exception {
// Generate a JWT using HMAC with an RSA public key to attempt to trick the library into verifying the JWT
// Testing for the vulnerability described by Tim McLean
// https://threatpost.com/critical-vulnerabilities-affect-json-web-token-libraries/111943/
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
JWT jwt = new JWT().setSubject("123456789");
// Hacked signer, obtain a publicly available RSA Public Key in use by the JWT issuer
Signer hackedSigner = HMACSigner.newSHA512Signer(new String(Files.readAllBytes(Paths.get("src/test/resources/rsa_public_key_2048.pem"))));
// Forged a JWT - sign your own token using the hacked Signer
String hmacSignedJWT = JWT.getEncoder().encode(jwt, hackedSigner, h -> h.set("kid", "abc"));
// Server side Verifiers used to validate JWTs they have issued
Verifier rsaVerifier = RSAVerifier.newVerifier(new String(Files.readAllBytes(Paths.get("src/test/resources/rsa_public_key_2048.pem"))));
Verifier hmacVerifier = HMACVerifier.newVerifier("secret");
// Attempt to decode using var-args call to decode, no kid. This correctly fails because we only ask the HMAC verifier to decode an HMAC signed JWT.
// And the server has built an HMAC verifier using their shared secret.
expectException(InvalidJWTSignatureException.class, () -> new JWTDecoder().decode(hmacSignedJWT, rsaVerifier, hmacVerifier));
Map<String, Verifier> verifierMap = new HashMap<>();
verifierMap.put("abc", rsaVerifier);
verifierMap.put("def", hmacVerifier);
// Attempt to decode using a map of verifiers. This correctly fails because the verifier for the kid does not support the algorithm in the header
// The kid in this case causes us to look up the verifier built by the server which is an RSA verifier.
expectException(MissingVerifierException.class, () -> new JWTDecoder().decode(hmacSignedJWT, verifierMap));
// Forge another JWT - but assume we know ahead of time all of the kids and which one maps to the hmac verifier
String hmacSignedJWTTakeTwo = JWT.getEncoder().encode(jwt, hackedSigner, h -> h.set("kid", "def"));
// This call fails because we ask the HMAC verifier to validate a signature built using a public key.
expectException(InvalidJWTSignatureException.class, () -> new JWTDecoder().decode(hmacSignedJWTTakeTwo, rsaVerifier, hmacVerifier));
// This call fails because in this case we have the correct kid 'def' which is the hmac verifier - but again the verifier was not built with the public key.
// The kid in this case causes us to look up the HMAC verifier which is what the hacker wants, but it again is already built using the correct shared secret.
expectException(InvalidJWTSignatureException.class, () -> new JWTDecoder().decode(hmacSignedJWTTakeTwo, verifierMap));
}
}