Skip to content

Commit 77fa61a

Browse files
committed
Limit the length of content codec list that can be processed automatically
1 parent 81b7971 commit 77fa61a

File tree

8 files changed

+75
-9
lines changed

8 files changed

+75
-9
lines changed

httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
package org.apache.hc.client5.http.impl;
2929

3030
import java.util.ArrayList;
31+
import java.util.Collection;
3132
import java.util.Collections;
3233
import java.util.List;
3334
import java.util.Locale;
3435

3536
import org.apache.hc.core5.annotation.Internal;
3637
import org.apache.hc.core5.http.EntityDetails;
38+
import org.apache.hc.core5.http.ProtocolException;
3739
import org.apache.hc.core5.http.message.MessageSupport;
3840
import org.apache.hc.core5.http.message.ParserCursor;
3941

@@ -43,6 +45,8 @@
4345
@Internal
4446
public final class ContentCodingSupport {
4547

48+
public static final int MAX_CODEC_LIST_LEN = 5;
49+
4650
private ContentCodingSupport() {
4751
}
4852

@@ -62,4 +66,10 @@ public static List<String> parseContentCodecs(final EntityDetails entityDetails)
6266
return codecs;
6367
}
6468

69+
public static void validate(final Collection<String> codecList, final int maxCodecListLen) throws ProtocolException {
70+
if (maxCodecListLen > 0 && codecList.size() > maxCodecListLen) {
71+
throw new ProtocolException("Codec list exceeds maximum of " + maxCodecListLen + " elements");
72+
}
73+
}
74+
6575
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,29 @@ public final class ContentCompressionAsyncExec implements AsyncExecChainHandler
6767

6868
private final Lookup<UnaryOperator<AsyncDataConsumer>> decoders;
6969
private final List<String> acceptTokens;
70+
private final int maxCodecListLen;
7071

7172
public ContentCompressionAsyncExec(
7273
final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> decoderMap,
73-
final boolean ignoreUnknown) {
74-
74+
final int maxCodecListLen) {
7575
Args.notEmpty(decoderMap, "Decoder map");
7676

7777
final RegistryBuilder<UnaryOperator<AsyncDataConsumer>> rb = RegistryBuilder.create();
7878
decoderMap.forEach(rb::register);
7979
this.decoders = rb.build();
8080
this.acceptTokens = new ArrayList<>(decoderMap.keySet());
81+
this.maxCodecListLen = maxCodecListLen;
82+
}
83+
84+
public ContentCompressionAsyncExec(
85+
final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> decoderMap) {
86+
this(decoderMap, ContentCodingSupport.MAX_CODEC_LIST_LEN);
8187
}
8288

8389
/**
8490
* Default: DEFLATE + GZIP (plus <code>x-gzip</code> alias).
8591
*/
86-
public ContentCompressionAsyncExec() {
92+
public ContentCompressionAsyncExec(final int maxCodecListLen) {
8793
final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
8894
map.put(ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
8995
map.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new);
@@ -109,8 +115,12 @@ public ContentCompressionAsyncExec() {
109115

110116
this.decoders = rb.build();
111117
this.acceptTokens = tokens;
118+
this.maxCodecListLen = maxCodecListLen;
112119
}
113120

121+
public ContentCompressionAsyncExec() {
122+
this(ContentCodingSupport.MAX_CODEC_LIST_LEN);
123+
}
114124

115125
@Override
116126
public void execute(

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,7 @@ public CloseableHttpAsyncClient build() {
11001100
if (!contentCompressionDisabled) {
11011101
if (contentDecoderMap != null && !contentDecoderMap.isEmpty()) {
11021102
execChainDefinition.addFirst(
1103-
new ContentCompressionAsyncExec(contentDecoderMap, true),
1103+
new ContentCompressionAsyncExec(contentDecoderMap),
11041104
ChainElement.COMPRESS.name());
11051105
} else {
11061106
execChainDefinition.addFirst(

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,25 @@ public final class ContentCompressionExec implements ExecChainHandler {
7272

7373
private final Header acceptEncoding;
7474
private final Lookup<UnaryOperator<HttpEntity>> decoderRegistry;
75+
private final int maxCodecListLen;
7576

7677
public ContentCompressionExec(
7778
final List<String> acceptEncoding,
78-
final Lookup<UnaryOperator<HttpEntity>> decoderRegistry) {
79+
final Lookup<UnaryOperator<HttpEntity>> decoderRegistry,
80+
final int maxCodecListLen) {
7981
this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING,
8082
Args.notEmpty(acceptEncoding, "Encoding list"));
8183
this.decoderRegistry = Args.notNull(decoderRegistry, "Decoder register");
84+
this.maxCodecListLen = maxCodecListLen;
8285
}
8386

84-
public ContentCompressionExec() {
87+
public ContentCompressionExec(
88+
final List<String> acceptEncoding,
89+
final Lookup<UnaryOperator<HttpEntity>> decoderRegistry) {
90+
this(acceptEncoding, decoderRegistry, ContentCodingSupport.MAX_CODEC_LIST_LEN);
91+
}
92+
93+
public ContentCompressionExec(final int maxCodecListLen) {
8594
final Map<ContentCoding, UnaryOperator<HttpEntity>> decoderMap = new EnumMap<>(ContentCoding.class);
8695
for (final ContentCoding c : ContentCoding.values()) {
8796
final UnaryOperator<HttpEntity> d = ContentCodecRegistry.decoder(c);
@@ -103,6 +112,11 @@ public ContentCompressionExec() {
103112
}
104113
this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptList);
105114
this.decoderRegistry = builder.build();
115+
this.maxCodecListLen = maxCodecListLen;
116+
}
117+
118+
public ContentCompressionExec() {
119+
this(ContentCodingSupport.MAX_CODEC_LIST_LEN);
106120
}
107121

108122
@Override
@@ -128,6 +142,7 @@ public ClassicHttpResponse execute(
128142
// check for zero length entity.
129143
if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
130144
final List<String> codecs = ContentCodingSupport.parseContentCodecs(entity);
145+
ContentCodingSupport.validate(codecs, maxCodecListLen);
131146
if (!codecs.isEmpty()) {
132147
for (int i = codecs.size() - 1; i >= 0; i--) {
133148
final String codec = codecs.get(i);

httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingAsyncEntityConsumerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public Set<String> getTrailerNames() {
204204
void unknownEncodingMapFlag() throws Exception {
205205
final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
206206
map.put("deflate", d -> new InflatingAsyncDataConsumer(d, null));
207-
final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map, false);
207+
final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map);
208208
assertNotNull(exec);
209209
}
210210
}

httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public Set<String> getTrailerNames() {
195195
void registerInExec() {
196196
final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
197197
map.put("br", InflatingBrotliDataConsumer::new);
198-
final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map, false);
198+
final ContentCompressionAsyncExec exec = new ContentCompressionAsyncExec(map);
199199
assertNotNull(exec);
200200
}
201201
}

httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public AsyncDataConsumer apply(final AsyncDataConsumer d) {
163163
return new InflatingAsyncDataConsumer(d, null);
164164
}
165165
});
166-
impl = new ContentCompressionAsyncExec(map, /*ignoreUnknown*/ false);
166+
impl = new ContentCompressionAsyncExec(map);
167167

168168
final HttpRequest request = new BasicHttpRequest(Method.GET, "/");
169169
final AsyncExecCallback cb = executeAndCapture(request);

httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.hc.core5.http.HttpException;
4141
import org.apache.hc.core5.http.HttpHost;
4242
import org.apache.hc.core5.http.Method;
43+
import org.apache.hc.core5.http.ProtocolException;
4344
import org.apache.hc.core5.http.io.entity.StringEntity;
4445
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
4546
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@@ -236,4 +237,34 @@ void testContentEncodingRequestParameter() throws Exception {
236237
Assertions.assertFalse(entity instanceof GzipDecompressingEntity);
237238
}
238239

240+
@Test
241+
void testContentEncodingExceedsCodecListLenMax() throws Exception {
242+
impl = new ContentCompressionExec(5);
243+
244+
final ClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, host, "/");
245+
final ClassicHttpResponse response1 = new BasicClassicHttpResponse(200, "OK");
246+
final HttpEntity original1 = EntityBuilder.create()
247+
.setText("encoded stuff")
248+
.setContentEncoding("gzip,gzip,gzip,gzip,gzip")
249+
.build();
250+
response1.setEntity(original1);
251+
252+
Mockito.when(execChain.proceed(request, scope)).thenReturn(response1);
253+
254+
final HttpEntity entity = response1.getEntity();
255+
Assertions.assertNotNull(entity);
256+
257+
final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
258+
final HttpEntity original2 = EntityBuilder.create()
259+
.setText("encoded stuff")
260+
.setContentEncoding("gzip,gzip,gzip,gzip,gzip,gzip")
261+
.build();
262+
response2.setEntity(original2);
263+
264+
Mockito.when(execChain.proceed(request, scope)).thenReturn(response2);
265+
266+
final ProtocolException exception = Assertions.assertThrows(ProtocolException.class, () -> impl.execute(request, scope, execChain));
267+
Assertions.assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage());
268+
}
269+
239270
}

0 commit comments

Comments
 (0)