Skip to content

Commit c63ea8e

Browse files
committed
Limit the length of content codec list that can be processed automatically
1 parent 378982f commit c63ea8e

File tree

8 files changed

+105
-9
lines changed

8 files changed

+105
-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: 14 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(
@@ -139,6 +149,7 @@ public AsyncDataConsumer handleResponse(final HttpResponse rsp,
139149
}
140150

141151
final List<String> codecs = ContentCodingSupport.parseContentCodecs(details);
152+
ContentCodingSupport.validate(codecs, maxCodecListLen);
142153
if (!codecs.isEmpty()) {
143154
AsyncDataConsumer downstream = cb.handleResponse(rsp, wrapEntityDetails(details));
144155
for (int i = codecs.size() - 1; i >= 0; i--) {

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
@@ -1098,7 +1098,7 @@ public CloseableHttpAsyncClient build() {
10981098
if (!contentCompressionDisabled) {
10991099
if (contentDecoderMap != null && !contentDecoderMap.isEmpty()) {
11001100
execChainDefinition.addFirst(
1101-
new ContentCompressionAsyncExec(contentDecoderMap, true),
1101+
new ContentCompressionAsyncExec(contentDecoderMap),
11021102
ChainElement.COMPRESS.name());
11031103
} else {
11041104
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: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.apache.hc.core5.http.HttpRequest;
5959
import org.apache.hc.core5.http.HttpResponse;
6060
import org.apache.hc.core5.http.Method;
61+
import org.apache.hc.core5.http.ProtocolException;
6162
import org.apache.hc.core5.http.message.BasicHttpRequest;
6263
import org.apache.hc.core5.http.message.BasicHttpResponse;
6364
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
@@ -166,7 +167,7 @@ public AsyncDataConsumer apply(final AsyncDataConsumer d) {
166167
return new InflatingAsyncDataConsumer(d, null);
167168
}
168169
});
169-
impl = new ContentCompressionAsyncExec(map, /*ignoreUnknown*/ false);
170+
impl = new ContentCompressionAsyncExec(map);
170171

171172
final HttpRequest request = new BasicHttpRequest(Method.GET, "/");
172173
final AsyncExecCallback cb = executeAndCapture(request);
@@ -188,4 +189,32 @@ void testCompressionDisabledViaRequestConfig() throws Exception {
188189

189190
assertFalse(request.containsHeader(HttpHeaders.ACCEPT_ENCODING));
190191
}
192+
193+
@Test
194+
void testContentEncodingExceedsCodecListLenMax() throws Exception {
195+
final HttpRequest request = new BasicHttpRequest(Method.GET, "/");
196+
final AsyncExecCallback cb = executeAndCapture(request);
197+
198+
final HttpResponse rsp1 = new BasicHttpResponse(200, "OK");
199+
final EntityDetails details1 = mock(EntityDetails.class);
200+
when(details1.getContentEncoding()).thenReturn("gzip,gzip,gzip,gzip,gzip");
201+
202+
final AsyncDataConsumer downstream1 = new StringAsyncEntityConsumer();
203+
when(originalCb.handleResponse(same(rsp1), same(details1))).thenReturn(downstream1);
204+
205+
final AsyncDataConsumer wrapped = cb.handleResponse(rsp1, details1);
206+
207+
assertNotNull(wrapped);
208+
209+
final HttpResponse rsp2 = new BasicHttpResponse(200, "OK");
210+
final EntityDetails details2 = mock(EntityDetails.class);
211+
when(details2.getContentEncoding()).thenReturn("gzip,gzip,gzip,gzip,gzip,gzip");
212+
213+
final AsyncDataConsumer downstream2 = new StringAsyncEntityConsumer();
214+
when(originalCb.handleResponse(same(rsp2), same(details2))).thenReturn(downstream2);
215+
216+
final ProtocolException exception = assertThrows(ProtocolException.class, () -> cb.handleResponse(rsp2, details2));
217+
assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage());
218+
}
219+
191220
}

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
@@ -39,6 +39,7 @@
3939
import org.apache.hc.core5.http.HttpException;
4040
import org.apache.hc.core5.http.HttpHost;
4141
import org.apache.hc.core5.http.Method;
42+
import org.apache.hc.core5.http.ProtocolException;
4243
import org.apache.hc.core5.http.io.entity.StringEntity;
4344
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
4445
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
@@ -234,4 +235,34 @@ void testContentEncodingRequestParameter() throws Exception {
234235
Assertions.assertSame(original, entity);
235236
}
236237

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

0 commit comments

Comments
 (0)