From 766040f98032cc53785177d9de7cb49b5c2379eb Mon Sep 17 00:00:00 2001 From: syed abdul khaliq Date: Mon, 26 Oct 2026 19:21:00 +0530 Subject: [PATCH] fix: reject non-canonical base64url signatures in verify --- .../auth0/jwt/algorithms/CryptoHelper.java | 18 ++++++++++++++++++ .../auth0/jwt/algorithms/ECDSAAlgorithm.java | 3 +-- .../auth0/jwt/algorithms/HMACAlgorithm.java | 3 +-- .../auth0/jwt/algorithms/NoneAlgorithm.java | 3 +-- .../auth0/jwt/algorithms/RSAAlgorithm.java | 3 +-- .../jwt/algorithms/ECDSAAlgorithmTest.java | 11 +++++++++++ .../jwt/algorithms/HMACAlgorithmTest.java | 19 +++++++++++++++++++ .../jwt/algorithms/RSAAlgorithmTest.java | 10 ++++++++++ 8 files changed, 62 insertions(+), 8 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index ecc945ee..79e0a907 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.AlgorithmParameterSpec; +import java.util.Base64; /** * Class used to perform the signature hash calculations. @@ -15,6 +16,23 @@ class CryptoHelper { private static final byte JWT_PART_SEPARATOR = (byte) 46; + /** + * Decode the JWT signature part, accepting only its canonical unpadded base64url form. + * The JDK decoder also accepts '=' padding and ignores non-zero trailing bits, which would + * let several different token strings carry the same valid signature. + * + * @param signature the base64url encoded JWT signature. + * @return the decoded signature bytes. + * @throws IllegalArgumentException if the signature is not canonical base64url without padding. + */ + static byte[] decodeSignature(String signature) throws IllegalArgumentException { + byte[] signatureBytes = Base64.getUrlDecoder().decode(signature); + if (!Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes).equals(signature)) { + throw new IllegalArgumentException("The signature is not a canonical base64url encoded value."); + } + return signatureBytes; + } + /** * Verify signature for JWT header and payload. * diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index b3046097..0214e299 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -11,7 +11,6 @@ import java.security.SignatureException; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; -import java.util.Base64; /** * Subclass representing an Elliptic Curve signing algorithm @@ -44,7 +43,7 @@ class ECDSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { - byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); + byte[] signatureBytes = CryptoHelper.decodeSignature(jwt.getSignature()); ECPublicKey publicKey = keyProvider.getPublicKeyById(jwt.getKeyId()); if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 0306e7c4..2b192273 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -8,7 +8,6 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import java.util.Base64; /** * Subclass representing an Hash-based MAC signing algorithm @@ -50,7 +49,7 @@ static byte[] getSecretBytes(String secret) throws IllegalArgumentException { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { - byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); + byte[] signatureBytes = CryptoHelper.decodeSignature(jwt.getSignature()); boolean valid = crypto.verifySignatureFor( getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java index 5c6c0fc5..097ab70f 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java @@ -3,7 +3,6 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import java.util.Base64; class NoneAlgorithm extends Algorithm { @@ -14,7 +13,7 @@ class NoneAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { - byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); + byte[] signatureBytes = CryptoHelper.decodeSignature(jwt.getSignature()); if (signatureBytes.length > 0) { throw new SignatureVerificationException(this); diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index b7135dc9..eb1f79c1 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -11,7 +11,6 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.AlgorithmParameterSpec; -import java.util.Base64; /** * Subclass representing an RSA signing algorithm @@ -54,7 +53,7 @@ class RSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { - byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); + byte[] signatureBytes = CryptoHelper.decodeSignature(jwt.getSignature()); RSAPublicKey publicKey = keyProvider.getPublicKeyById(jwt.getKeyId()); if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 2e636c71..d51a9149 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -68,6 +68,17 @@ public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { algorithm.verify(JWT.decode(jwt)); } + @Test + public void shouldFailECDSA256VerificationWithNonCanonicalSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + // Same signature bytes as the valid token above, but the unused low bits of the last character are set + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1h"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + Algorithm algorithm = Algorithm.ECDSA256(key); + algorithm.verify(JWT.decode(jwt)); + } + @Test public void shouldThrowOnECDSA256VerificationWithDERSignature() throws Exception { exception.expect(SignatureVerificationException.class); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index 9b6ac0c0..b0773de3 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -79,6 +79,25 @@ public void shouldFailHMAC256VerificationWithInvalidSecretBytes() { algorithm.verify(JWT.decode(jwt)); } + @Test + public void shouldFailHMAC256VerificationWithNonCanonicalSignatureTrailingBits() { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256"); + // Same signature bytes as the valid token above, but the unused low bits of the last character are set + String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1N"; + Algorithm algorithm = Algorithm.HMAC256("secret"); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailHMAC256VerificationWithPaddedSignature() { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256"); + String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M="; + Algorithm algorithm = Algorithm.HMAC256("secret"); + algorithm.verify(JWT.decode(jwt)); + } + @Test public void shouldPassHMAC384Verification() { String jwt = "eyJhbGciOiJIUzM4NCIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.uztpK_wUMYJhrRv8SV-1LU4aPnwl-EM1q-wJnqgyb5DHoDteP6lN_gE1xnZJH5vw"; diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java index 63ea45e7..83b8a9aa 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -43,6 +43,16 @@ public void shouldPassRSA256Verification() throws Exception { algorithm.verify(JWT.decode(jwt)); } + @Test + public void shouldFailRSA256VerificationWithNonCanonicalSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA"); + // Same signature bytes as the valid token above, but the unused low bits of the last character are set + String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cB"; + Algorithm algorithm = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); + algorithm.verify(JWT.decode(jwt)); + } + @Test public void shouldPassRSA256VerificationWithBothKeys() throws Exception { String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA";