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/17 09:18:31 UTC

(httpcomponents-client) 01/01: 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 master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit c63ea8ea3be1bc8f8b1ea86f5ec3c89dc64bcfea
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    | 17 +++++++++---
 .../http/impl/async/HttpAsyncClientBuilder.java    |  2 +-
 .../http/impl/classic/ContentCompressionExec.java  | 19 +++++++++++--
 .../methods/InflatingAsyncEntityConsumerTest.java  |  2 +-
 .../methods/InflatingBrotliDataConsumerTest.java   |  2 +-
 .../async/TestContentCompressionAsyncExec.java     | 31 +++++++++++++++++++++-
 .../impl/classic/TestContentCompressionExec.java   | 31 ++++++++++++++++++++++
 8 files changed, 105 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..d685b39cd 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(
@@ -139,6 +149,7 @@ public AsyncDataConsumer handleResponse(final HttpResponse rsp,
                 }
 
                 final List<String> codecs = ContentCodingSupport.parseContentCodecs(details);
+                ContentCodingSupport.validate(codecs, maxCodecListLen);
                 if (!codecs.isEmpty()) {
                     AsyncDataConsumer downstream = cb.handleResponse(rsp, wrapEntityDetails(details));
                     for (int i = codecs.size() - 1; i >= 0; i--) {
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 e38576d8e..b29ca5f76 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
@@ -1098,7 +1098,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 cec7ba396..3d9360571 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
@@ -58,6 +58,7 @@
 import org.apache.hc.core5.http.HttpRequest;
 import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.ProtocolException;
 import org.apache.hc.core5.http.message.BasicHttpRequest;
 import org.apache.hc.core5.http.message.BasicHttpResponse;
 import org.apache.hc.core5.http.nio.AsyncDataConsumer;
@@ -166,7 +167,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);
@@ -188,4 +189,32 @@ void testCompressionDisabledViaRequestConfig() throws Exception {
 
         assertFalse(request.containsHeader(HttpHeaders.ACCEPT_ENCODING));
     }
+
+    @Test
+    void testContentEncodingExceedsCodecListLenMax() throws Exception {
+        final HttpRequest request = new BasicHttpRequest(Method.GET, "/");
+        final AsyncExecCallback cb = executeAndCapture(request);
+
+        final HttpResponse rsp1 = new BasicHttpResponse(200, "OK");
+        final EntityDetails details1 = mock(EntityDetails.class);
+        when(details1.getContentEncoding()).thenReturn("gzip,gzip,gzip,gzip,gzip");
+
+        final AsyncDataConsumer downstream1 = new StringAsyncEntityConsumer();
+        when(originalCb.handleResponse(same(rsp1), same(details1))).thenReturn(downstream1);
+
+        final AsyncDataConsumer wrapped = cb.handleResponse(rsp1, details1);
+
+        assertNotNull(wrapped);
+
+        final HttpResponse rsp2 = new BasicHttpResponse(200, "OK");
+        final EntityDetails details2 = mock(EntityDetails.class);
+        when(details2.getContentEncoding()).thenReturn("gzip,gzip,gzip,gzip,gzip,gzip");
+
+        final AsyncDataConsumer downstream2 = new StringAsyncEntityConsumer();
+        when(originalCb.handleResponse(same(rsp2), same(details2))).thenReturn(downstream2);
+
+        final ProtocolException exception = assertThrows(ProtocolException.class, () -> cb.handleResponse(rsp2, details2));
+        assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage());
+    }
+
 }
\ No newline at end of file
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 fa6ab190e..89a9606df 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
@@ -39,6 +39,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;
@@ -234,4 +235,34 @@ void testContentEncodingRequestParameter() throws Exception {
         Assertions.assertSame(original, entity);
     }
 
+    @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());
+    }
+
 }