You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2025/12/16 20:09:07 UTC

(httpcomponents-client) 01/02: Limit the length of content codec list that can be processed automatically

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch 5.6.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit 77fa61aae9fe492eb81e321073441646856775e1
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon Dec 15 10:23:19 2025 +0100

    Limit the length of content codec list that can be processed automatically
---
 .../hc/client5/http/impl/ContentCodingSupport.java | 10 +++++++
 .../impl/async/ContentCompressionAsyncExec.java    | 16 ++++++++---
 .../http/impl/async/HttpAsyncClientBuilder.java    |  2 +-
 .../http/impl/classic/ContentCompressionExec.java  | 19 +++++++++++--
 .../methods/InflatingAsyncEntityConsumerTest.java  |  2 +-
 .../methods/InflatingBrotliDataConsumerTest.java   |  2 +-
 .../async/TestContentCompressionAsyncExec.java     |  2 +-
 .../impl/classic/TestContentCompressionExec.java   | 31 ++++++++++++++++++++++
 8 files changed, 75 insertions(+), 9 deletions(-)

diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java
index 441c0d329..5d794b9f3 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java
@@ -28,12 +28,14 @@
 package org.apache.hc.client5.http.impl;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 
 import org.apache.hc.core5.annotation.Internal;
 import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.message.MessageSupport;
 import org.apache.hc.core5.http.message.ParserCursor;
 
@@ -43,6 +45,8 @@
 @Internal
 public final class ContentCodingSupport {
 
+    public static final int MAX_CODEC_LIST_LEN = 5;
+
     private ContentCodingSupport() {
     }
 
@@ -62,4 +66,10 @@ public static List<String> parseContentCodecs(final EntityDetails entityDetails)
         return codecs;
     }
 
+    public static void validate(final Collection<String> codecList, final int maxCodecListLen) throws ProtocolException {
+        if (maxCodecListLen > 0 && codecList.size() > maxCodecListLen) {
+            throw new ProtocolException("Codec list exceeds maximum of " + maxCodecListLen + " elements");
+        }
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java
index d8c9a6aa4..04d5ed796 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java
@@ -67,23 +67,29 @@ public final class ContentCompressionAsyncExec implements AsyncExecChainHandler
 
     private final Lookup<UnaryOperator<AsyncDataConsumer>> decoders;
     private final List<String> acceptTokens;
+    private final int maxCodecListLen;
 
     public ContentCompressionAsyncExec(
             final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> decoderMap,
-            final boolean ignoreUnknown) {
-
+            final int maxCodecListLen) {
         Args.notEmpty(decoderMap, "Decoder map");
 
         final RegistryBuilder<UnaryOperator<AsyncDataConsumer>> rb = RegistryBuilder.create();
         decoderMap.forEach(rb::register);
         this.decoders = rb.build();
         this.acceptTokens = new ArrayList<>(decoderMap.keySet());
+        this.maxCodecListLen = maxCodecListLen;
+    }
+
+    public ContentCompressionAsyncExec(
+            final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> decoderMap) {
+        this(decoderMap, ContentCodingSupport.MAX_CODEC_LIST_LEN);
     }
 
     /**
      * Default: DEFLATE + GZIP (plus <code>x-gzip</code> alias).
      */
-    public ContentCompressionAsyncExec() {
+    public ContentCompressionAsyncExec(final int maxCodecListLen) {
         final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
         map.put(ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
         map.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new);
@@ -109,8 +115,12 @@ public ContentCompressionAsyncExec() {
 
         this.decoders = rb.build();
         this.acceptTokens = tokens;
+        this.maxCodecListLen = maxCodecListLen;
     }
 
+    public ContentCompressionAsyncExec() {
+        this(ContentCodingSupport.MAX_CODEC_LIST_LEN);
+    }
 
     @Override
     public void execute(
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
index b2f55bba7..d2968a5bf 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
@@ -1100,7 +1100,7 @@ public CloseableHttpAsyncClient build() {
         if (!contentCompressionDisabled) {
             if (contentDecoderMap != null && !contentDecoderMap.isEmpty()) {
                 execChainDefinition.addFirst(
-                        new ContentCompressionAsyncExec(contentDecoderMap, true),
+                        new ContentCompressionAsyncExec(contentDecoderMap),
                         ChainElement.COMPRESS.name());
             } else {
                 execChainDefinition.addFirst(
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
index f4da426ec..fc7ed56c0 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
@@ -72,16 +72,25 @@ public final class ContentCompressionExec implements ExecChainHandler {
 
     private final Header acceptEncoding;
     private final Lookup<UnaryOperator<HttpEntity>> decoderRegistry;
+    private final int maxCodecListLen;
 
     public ContentCompressionExec(
             final List<String> acceptEncoding,
-            final Lookup<UnaryOperator<HttpEntity>> decoderRegistry) {
+            final Lookup<UnaryOperator<HttpEntity>> decoderRegistry,
+            final int maxCodecListLen) {
         this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING,
                 Args.notEmpty(acceptEncoding, "Encoding list"));
         this.decoderRegistry = Args.notNull(decoderRegistry, "Decoder register");
+        this.maxCodecListLen = maxCodecListLen;
     }
 
-    public ContentCompressionExec() {
+    public ContentCompressionExec(
+            final List<String> acceptEncoding,
+            final Lookup<UnaryOperator<HttpEntity>> decoderRegistry) {
+        this(acceptEncoding, decoderRegistry, ContentCodingSupport.MAX_CODEC_LIST_LEN);
+    }
+
+    public ContentCompressionExec(final int maxCodecListLen) {
         final Map<ContentCoding, UnaryOperator<HttpEntity>> decoderMap = new EnumMap<>(ContentCoding.class);
         for (final ContentCoding c : ContentCoding.values()) {
             final UnaryOperator<HttpEntity> d = ContentCodecRegistry.decoder(c);
@@ -103,6 +112,11 @@ public ContentCompressionExec() {
         }
         this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptList);
         this.decoderRegistry = builder.build();
+        this.maxCodecListLen = maxCodecListLen;
+    }
+
+    public ContentCompressionExec() {
+        this(ContentCodingSupport.MAX_CODEC_LIST_LEN);
     }
 
     @Override
@@ -128,6 +142,7 @@ public ClassicHttpResponse execute(
         // check for zero length entity.
         if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
             final List<String> codecs = ContentCodingSupport.parseContentCodecs(entity);
+            ContentCodingSupport.validate(codecs, maxCodecListLen);
             if (!codecs.isEmpty()) {
                 for (int i = codecs.size() - 1; i >= 0; i--) {
                     final String codec = codecs.get(i);
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingAsyncEntityConsumerTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingAsyncEntityConsumerTest.java
index fca30dc82..308bfe4cf 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingAsyncEntityConsumerTest.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingAsyncEntityConsumerTest.java
@@ -204,7 +204,7 @@ public Set<String> getTrailerNames() {
     void unknownEncodingMapFlag() throws Exception {
         final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
         map.put("deflate", d -> new InflatingAsyncDataConsumer(d, null));
-        final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map, false);
+        final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map);
         assertNotNull(exec);
     }
 }
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumerTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumerTest.java
index 8446d2830..230e0936e 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumerTest.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumerTest.java
@@ -195,7 +195,7 @@ public Set<String> getTrailerNames() {
     void registerInExec() {
         final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
         map.put("br", InflatingBrotliDataConsumer::new);
-        final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map, false);
+        final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map);
         assertNotNull(exec);
     }
 }
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java
index a02ddb45d..8cf3a181d 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java
@@ -163,7 +163,7 @@ public AsyncDataConsumer apply(final AsyncDataConsumer d) {
                 return new InflatingAsyncDataConsumer(d, null);
             }
         });
-        impl = new ContentCompressionAsyncExec(map, /*ignoreUnknown*/ false);
+        impl = new ContentCompressionAsyncExec(map);
 
         final HttpRequest request = new BasicHttpRequest(Method.GET, "/");
         final AsyncExecCallback cb = executeAndCapture(request);
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java
index d9c5151d6..44711e82f 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java
@@ -40,6 +40,7 @@
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
 import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@@ -236,4 +237,34 @@ void testContentEncodingRequestParameter() throws Exception {
         Assertions.assertFalse(entity instanceof GzipDecompressingEntity);
     }
 
+    @Test
+    void testContentEncodingExceedsCodecListLenMax() throws Exception {
+        impl = new ContentCompressionExec(5);
+
+        final ClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, host, "/");
+        final ClassicHttpResponse response1 = new BasicClassicHttpResponse(200, "OK");
+        final HttpEntity original1 = EntityBuilder.create()
+                .setText("encoded stuff")
+                .setContentEncoding("gzip,gzip,gzip,gzip,gzip")
+                .build();
+        response1.setEntity(original1);
+
+        Mockito.when(execChain.proceed(request, scope)).thenReturn(response1);
+
+        final HttpEntity entity = response1.getEntity();
+        Assertions.assertNotNull(entity);
+
+        final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
+        final HttpEntity original2 = EntityBuilder.create()
+                .setText("encoded stuff")
+                .setContentEncoding("gzip,gzip,gzip,gzip,gzip,gzip")
+                .build();
+        response2.setEntity(original2);
+
+        Mockito.when(execChain.proceed(request, scope)).thenReturn(response2);
+
+        final ProtocolException exception = Assertions.assertThrows(ProtocolException.class, () -> impl.execute(request, scope, execChain));
+        Assertions.assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage());
+    }
+
 }