diff --git a/CHANGELOG.md b/CHANGELOG.md index d036f30cb..e99faea43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Release Notes +### 0.11.3 + +This patch release: + +* Adds handling for common JSON parsing exceptions and wraps them in a `JwtException`. + ### 0.11.2 This patch release: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 6034b044b..591433f3c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -42,7 +42,6 @@ import io.jsonwebtoken.impl.lang.LegacyServices; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.DateFormats; @@ -111,7 +110,7 @@ public DefaultJwtParser() { } @Override public JwtParser deserializeJsonWith(Deserializer> deserializer) { Assert.notNull(deserializer, "deserializer cannot be null."); - this.deserializer = deserializer; + this.deserializer = new JwtDeserializer<>(deserializer); return this; } @@ -253,7 +252,7 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, if (this.deserializer == null) { // try to find one based on the services available // TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0 - this.deserializer = LegacyServices.loadFirst(Deserializer.class); + this.deserializeJsonWith(LegacyServices.loadFirst(Deserializer.class)); } Assert.hasText(jwt, "JWT String argument cannot be null or empty."); @@ -617,11 +616,7 @@ public Jws onClaimsJws(Jws jws) { @SuppressWarnings("unchecked") protected Map readValue(String val) { - try { - byte[] bytes = val.getBytes(Strings.UTF_8); - return deserializer.deserialize(bytes); - } catch (DeserializationException e) { - throw new MalformedJwtException("Unable to read JSON value: " + val, e); - } + byte[] bytes = val.getBytes(Strings.UTF_8); + return deserializer.deserialize(bytes); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index e03c5f4c8..3b299ebfb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -204,7 +204,7 @@ public JwtParser build() { allowedClockSkewMillis, expectedClaims, base64UrlDecoder, - deserializer, + new JwtDeserializer<>(deserializer), compressionCodecResolver)); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtDeserializer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtDeserializer.java new file mode 100644 index 000000000..713e6b114 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtDeserializer.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * 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.jsonwebtoken.impl; + +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.io.DeserializationException; +import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.io.IOException; +import io.jsonwebtoken.lang.Assert; + +import java.nio.charset.StandardCharsets; + +/** + * A {@link Deserializer} implementation that wraps another Deserializer implementation to adds common JWT related + * error handling. + * @param type of object to deserialize. + * @since 0.11.3 + */ +class JwtDeserializer implements Deserializer { + + static final String MALFORMED_ERROR = "Malformed JWT JSON: "; + static final String MALFORMED_COMPLEX_ERROR = "Malformed or excessively complex JWT JSON. This could reflect a potential malicious JWT, please investigate the JWT source further. JSON: "; + + private final Deserializer deserializer; + + JwtDeserializer(Deserializer deserializer) { + Assert.notNull(deserializer, "deserializer cannot be null."); + this.deserializer = deserializer; + } + + @Override + public T deserialize(byte[] bytes) throws DeserializationException { + try { + return deserializer.deserialize(bytes); + } catch (DeserializationException e) { + throw new MalformedJwtException(MALFORMED_ERROR + new String(bytes, StandardCharsets.UTF_8), e); + } catch (StackOverflowError e) { + throw new IOException(MALFORMED_COMPLEX_ERROR + new String(bytes, StandardCharsets.UTF_8), e); + } + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy index e7d5c677b..fa8f717eb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/DeprecatedJwtParserTest.groovy @@ -83,7 +83,7 @@ class DeprecatedJwtParserTest { Jwts.parser().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Unable to read JSON value: ' + junkPayload + assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 39d8f00c0..e5b46bb64 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -86,7 +86,7 @@ class JwtParserTest { Jwts.parserBuilder().build().parse(bad) fail() } catch (MalformedJwtException expected) { - assertEquals expected.getMessage(), 'Unable to read JSON value: ' + junkPayload + assertEquals expected.getMessage(), 'Malformed JWT JSON: ' + junkPayload } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index 3a0940075..cad6d3d82 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -29,6 +29,7 @@ import javax.crypto.SecretKey import static org.junit.Assert.assertEquals import static org.junit.Assert.assertSame +import static org.junit.Assert.assertTrue // NOTE to the casual reader: even though this test class appears mostly empty, the DefaultJwtParser // implementation is tested to 100% coverage. The vast majority of its tests are in the JwtsTest class. This class @@ -69,7 +70,8 @@ class DefaultJwtParserTest { } } def p = new DefaultJwtParser().deserializeJsonWith(deserializer) - assertSame deserializer, p.deserializer + assertTrue("Expected wrapping deserializer to be instance of JwtDeserializer", p.deserializer instanceof JwtDeserializer ) + assertSame deserializer, p.deserializer.deserializer def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtDeserializerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtDeserializerTest.groovy new file mode 100644 index 000000000..50ee7707b --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtDeserializerTest.groovy @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 jsonwebtoken.io + * + * 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.jsonwebtoken.impl + +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.io.DeserializationException +import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.IOException +import org.junit.Assert +import org.junit.Test + +import java.nio.charset.StandardCharsets + +import static org.easymock.EasyMock.expect +import static org.easymock.EasyMock.mock +import static org.easymock.EasyMock.replay +import static org.junit.Assert.assertEquals + +class JwtDeserializerTest { + + /** + * It's common for JSON parser's to throw a StackOverflowError when body is deeply nested. Since it's common + * across multiple parsers, JJWT handles the exception when parsing. + */ + @Test + void testParserStackOverflowError() { + + byte[] jsonBytes = '{"test": "testParserStackOverflowError"}'.getBytes(StandardCharsets.UTF_8) + + // create a Deserializer that will throw a StackOverflowError + Deserializer> deserializer = mock(Deserializer) + expect(deserializer.deserialize(jsonBytes)).andThrow(new StackOverflowError("Test exception: testParserStackOverflowError" )) + replay(deserializer) + + try { + new JwtDeserializer<>(deserializer).deserialize(jsonBytes) + Assert.fail("Expected IOException") + } catch (IOException e) { + assertEquals JwtDeserializer.MALFORMED_COMPLEX_ERROR + new String(jsonBytes), e.message + } + } + + /** + * Check that a DeserializationException, is wrapped and rethrown as a MalformedJwtException with a developer friendly message. + */ + @Test + void testDeserializationExceptionMessage() { + + byte[] jsonBytes = '{"test": "testDeserializationExceptionMessage"}'.getBytes(StandardCharsets.UTF_8) + + // create a Deserializer that will throw a DeserializationException + Deserializer> deserializer = mock(Deserializer) + expect(deserializer.deserialize(jsonBytes)).andThrow(new DeserializationException("Test exception: testDeserializationExceptionMessage" )) + replay(deserializer) + + try { + new JwtDeserializer<>(deserializer).deserialize(jsonBytes) + Assert.fail("Expected MalformedJwtException") + } catch (MalformedJwtException e) { + assertEquals JwtDeserializer.MALFORMED_ERROR + new String(jsonBytes), e.message + } + } +}