[FIX] JWT should not attempt to unzip data by default (#2189)
diff --git a/pom.xml b/pom.xml
index 5ea2731..1113e01 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2409,7 +2409,6 @@
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
- <scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
diff --git a/server/apps/cassandra-app/sample-configuration/jvm.properties b/server/apps/cassandra-app/sample-configuration/jvm.properties
index 9415002..03997ab 100644
--- a/server/apps/cassandra-app/sample-configuration/jvm.properties
+++ b/server/apps/cassandra-app/sample-configuration/jvm.properties
@@ -62,4 +62,7 @@
# Value from which dedicated BodyFactory shall start buffering data to a file.
# Used for attachment parsing upon message creation. Default value: 100K.
-# james.mime4j.buffered.body.factory.file.threshold=100K
\ No newline at end of file
+# james.mime4j.buffered.body.factory.file.threshold=100K
+
+# Whether James should unzip JWTs. Default to false
+# james.jwt.zip.allow=false
\ No newline at end of file
diff --git a/server/apps/distributed-pop3-app/sample-configuration/jvm.properties b/server/apps/distributed-pop3-app/sample-configuration/jvm.properties
index 3676aa5..181a7c3 100644
--- a/server/apps/distributed-pop3-app/sample-configuration/jvm.properties
+++ b/server/apps/distributed-pop3-app/sample-configuration/jvm.properties
@@ -53,3 +53,6 @@
# Disable Remote Code Execution feature from JMX
# CF https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/19fb8f93c59dfd791f62d41f332db9e306bc1422/src/java.management/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java#L646
jmx.remote.x.mlet.allow.getMBeansFromURL=false
+
+# Whether James should unzip JWTs. Default to false
+# james.jwt.zip.allow=false
\ No newline at end of file
diff --git a/server/apps/jpa-app/sample-configuration/jvm.properties b/server/apps/jpa-app/sample-configuration/jvm.properties
index 7154210..4ebef36 100644
--- a/server/apps/jpa-app/sample-configuration/jvm.properties
+++ b/server/apps/jpa-app/sample-configuration/jvm.properties
@@ -50,4 +50,7 @@
# Disable Remote Code Execution feature from JMX
# CF https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/19fb8f93c59dfd791f62d41f332db9e306bc1422/src/java.management/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java#L646
jmx.remote.x.mlet.allow.getMBeansFromURL=false
-openjpa.Multithreaded=true
\ No newline at end of file
+openjpa.Multithreaded=true
+
+# Whether James should unzip JWTs. Default to false
+# james.jwt.zip.allow=false
\ No newline at end of file
diff --git a/server/apps/jpa-smtp-app/sample-configuration/jvm.properties b/server/apps/jpa-smtp-app/sample-configuration/jvm.properties
index 7154210..4ebef36 100644
--- a/server/apps/jpa-smtp-app/sample-configuration/jvm.properties
+++ b/server/apps/jpa-smtp-app/sample-configuration/jvm.properties
@@ -50,4 +50,7 @@
# Disable Remote Code Execution feature from JMX
# CF https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/19fb8f93c59dfd791f62d41f332db9e306bc1422/src/java.management/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java#L646
jmx.remote.x.mlet.allow.getMBeansFromURL=false
-openjpa.Multithreaded=true
\ No newline at end of file
+openjpa.Multithreaded=true
+
+# Whether James should unzip JWTs. Default to false
+# james.jwt.zip.allow=false
\ No newline at end of file
diff --git a/server/apps/memory-app/sample-configuration/jvm.properties b/server/apps/memory-app/sample-configuration/jvm.properties
index 8a4c348..1834ee9 100644
--- a/server/apps/memory-app/sample-configuration/jvm.properties
+++ b/server/apps/memory-app/sample-configuration/jvm.properties
@@ -52,4 +52,7 @@
jmx.remote.x.mlet.allow.getMBeansFromURL=false
# Default charset to use in JMAP to present text body parts
-# james.jmap.default.charset=US-ASCII
\ No newline at end of file
+# james.jmap.default.charset=US-ASCII
+
+# Whether James should unzip JWTs. Default to false
+# james.jwt.zip.allow=false
\ No newline at end of file
diff --git a/server/apps/scaling-pulsar-smtp/sample-configuration/jvm.properties b/server/apps/scaling-pulsar-smtp/sample-configuration/jvm.properties
index 4fb3f69..df272a1 100644
--- a/server/apps/scaling-pulsar-smtp/sample-configuration/jvm.properties
+++ b/server/apps/scaling-pulsar-smtp/sample-configuration/jvm.properties
@@ -43,3 +43,6 @@
# JMX, when enable causes RMI to plan System.gc every hour. Set this instead to once every 1000h.
#sun.rmi.dgc.server.gcInterval=3600000000
#sun.rmi.dgc.client.gcInterval=3600000000
+
+# Whether James should unzip JWTs. Default to false
+# james.jwt.zip.allow=false
diff --git a/server/protocols/jwt/pom.xml b/server/protocols/jwt/pom.xml
index 1828858..157f6f0 100644
--- a/server/protocols/jwt/pom.xml
+++ b/server/protocols/jwt/pom.xml
@@ -69,7 +69,6 @@
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
- <scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
index c5bc4ff..878a8a6 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
@@ -25,16 +25,36 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.CompressionCodecResolver;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
public class JwtTokenVerifier {
+ private static final CompressionCodecResolver DEFAULT_COMPRESSION_CODEC_RESOLVER = new DefaultCompressionCodecResolver();
+ private static final CompressionCodecResolver SECURE_COMPRESSION_CODEC_RESOLVER = header -> {
+ if (Optional.ofNullable(header.getCompressionAlgorithm()).isPresent()) {
+ throw new RuntimeException("Rejecting a ZIP JWT. Usage of ZIPPED JWT can result in " +
+ "excessive memory usage with malicious JWT tokens. To activate support for ZIPPed" +
+ "JWT please run James with the -Djames.jwt.zip.allow=true system property.");
+ }
+ return DEFAULT_COMPRESSION_CODEC_RESOLVER.resolveCompressionCodec(header);
+ };
+ private static final boolean allowZipJWT = Optional.ofNullable(System.getProperty("james.jwt.zip.allow"))
+ .map(Boolean::parseBoolean)
+ .orElse(false);
+ @VisibleForTesting
+ static CompressionCodecResolver CONFIGURED_COMPRESSION_CODEC_RESOLVER = Optional.of(allowZipJWT)
+ .filter(b -> b)
+ .map(any -> DEFAULT_COMPRESSION_CODEC_RESOLVER)
+ .orElse(SECURE_COMPRESSION_CODEC_RESOLVER);
public interface Factory {
JwtTokenVerifier create();
@@ -97,6 +117,7 @@
private JwtParser toImmutableJwtParser(PublicKey publicKey) {
return Jwts.parserBuilder()
.setSigningKey(publicKey)
+ .setCompressionCodecResolver(CONFIGURED_COMPRESSION_CODEC_RESOLVER)
.build();
}
}
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java
index ff18fcf..0d146a8 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java
@@ -22,6 +22,7 @@
import static org.apache.james.jwt.OidcTokenFixture.INTROSPECTION_RESPONSE;
import static org.apache.james.jwt.OidcTokenFixture.USERINFO_RESPONSE;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.net.MalformedURLException;
@@ -40,6 +41,10 @@
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
+import io.jsonwebtoken.CompressionCodecs;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
import reactor.core.publisher.Mono;
class OidcJwtTokenVerifierTest {
@@ -87,6 +92,33 @@
}
@Test
+ void shouldRejectZippedJWTByDefault() {
+ String jws = Jwts.builder()
+ .claim("kid", "a".repeat(100))
+ .compressWith(CompressionCodecs.DEFLATE)
+ .signWith(SignatureAlgorithm.HS256, OidcTokenFixture.PRIVATE_KEY_BASE64.replace("\n", ""))
+ .compact();
+
+ assertThatThrownBy(() -> OidcJwtTokenVerifier.verifySignatureAndExtractClaim(jws, getJwksURL(), "kid"))
+ .isInstanceOf(RuntimeException.class)
+ .hasMessageContaining("Rejecting a ZIP JWT");
+ }
+
+ @Test
+ void shouldAcceptZippedJWTWhenConfigured() {
+ String jws = Jwts.builder()
+ .claim("kid", "a".repeat(100))
+ .compressWith(CompressionCodecs.DEFLATE)
+ .signWith(SignatureAlgorithm.HS256, OidcTokenFixture.PRIVATE_KEY_BASE64.replace("\n", ""))
+ .compact();
+
+ JwtTokenVerifier.CONFIGURED_COMPRESSION_CODEC_RESOLVER = new DefaultCompressionCodecResolver();
+
+ assertThatCode(() -> OidcJwtTokenVerifier.verifySignatureAndExtractClaim(jws, getJwksURL(), "kid"))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
void verifyAndClaimShouldReturnEmptyWhenValidTokenHasNotFoundKid() {
assertThat(OidcJwtTokenVerifier.verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN_HAS_NOT_FOUND_KID, getJwksURL(), "email_address"))
.isEmpty();
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
index eafd93a..d07646e 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
@@ -21,8 +21,7 @@
public class OidcTokenFixture {
- public static final String PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" +
- "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCroSIEhNYajXzC\n" +
+ public static final String PRIVATE_KEY_BASE64 = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCroSIEhNYajXzC\n" +
"gsn+xetgjqYc/SaihaHCIjWra2xMbkyl42BITRmjBFGbUxThMEg5YvaBXC1XQeib\n" +
"auW7gJnBZs8S54K5FMyjgUXOKbjHqPRxE76vUaIYkAZoeufAnXosfDf/XUZTTKE2\n" +
"yxyZJhdfgU/RSEpN19joXfskIQWmIXlMKkIG9lGqj7eIcyomdlHHuYxb9owqU+lP\n" +
@@ -47,7 +46,9 @@
"EdbOTUKhdenKEcSvICOjCrRL/sZHQCSZCH+d7UkCgYAbGniqH/pp73sGd9NZyVT/\n" +
"HJdOH5dfBfS9sBBJ1f0/pySJLKcArOXS9BMIFueOq4EIc+7hKDCQuqeyhpYZ6UCe\n" +
"C9h0QNig49qGI/UEtlNrIlydHyPinTa1fDqu99EuRHG0d4RuONW45tmZAY7mGIbf\n" +
- "PRhJhwOHZT9xO+uPrtQIAw==\n" +
+ "PRhJhwOHZT9xO+uPrtQIAw==\n";
+ public static final String PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" +
+ PRIVATE_KEY_BASE64 +
"-----END PRIVATE KEY-----";
public static final String PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" +