Skip to content

Commit 0e96329

Browse files
HTTP/2: validate trailer header fields (#623)
Reject pseudo-headers in inbound and outbound trailers
1 parent cd18a1c commit 0e96329

6 files changed

Lines changed: 159 additions & 0 deletions

File tree

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,7 @@ public void endStream(final List<? extends Header> trailers) throws IOException
15551555
ensureNotClosed();
15561556
localClosed = true;
15571557
if (trailers != null && !trailers.isEmpty()) {
1558+
TrailersValidationSupport.verify(trailers);
15581559
commitHeaders(id, trailers, true);
15591560
} else {
15601561
final RawFrame frame = frameFactory.createData(id, null, true);

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ public void consumeHeader(final List<Header> headers, final boolean endStream) t
216216
responseState.set(endStream ? MessageState.COMPLETE : MessageState.BODY);
217217
break;
218218
case BODY:
219+
TrailersValidationSupport.verify(headers);
219220
responseState.set(MessageState.COMPLETE);
220221
exchangeHandler.streamEnd(headers);
221222
break;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.core5.http2.impl.nio;
28+
29+
import java.util.List;
30+
31+
import org.apache.hc.core5.annotation.Internal;
32+
import org.apache.hc.core5.http.Header;
33+
import org.apache.hc.core5.http2.H2ConnectionException;
34+
import org.apache.hc.core5.http2.H2Error;
35+
36+
37+
@Internal
38+
final class TrailersValidationSupport {
39+
40+
static void verify(final List<? extends Header> trailers) throws H2ConnectionException {
41+
if (trailers == null || trailers.isEmpty()) {
42+
return;
43+
}
44+
for (int i = 0; i < trailers.size(); i++) {
45+
final String name = trailers.get(i).getName();
46+
if (name != null && !name.isEmpty() && name.charAt(0) == ':') {
47+
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Pseudo-header '" + name + "' is not allowed in trailers");
48+
}
49+
}
50+
}
51+
52+
53+
private TrailersValidationSupport() {
54+
}
55+
56+
}

httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,27 @@ void testStreamIdleTimeoutTriggersH2StreamTimeoutException() throws Exception {
10781078
Assertions.assertEquals(1, timeoutEx.getStreamId());
10791079

10801080
}
1081+
1082+
@Test
1083+
void testOutboundTrailersWithPseudoHeaderRejected() throws Exception {
1084+
final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl(
1085+
protocolIOSession,
1086+
FRAME_FACTORY,
1087+
StreamIdGenerator.EVEN,
1088+
httpProcessor,
1089+
CharCodingConfig.DEFAULT,
1090+
H2Config.custom().build(),
1091+
h2StreamListener,
1092+
() -> streamHandler);
1093+
1094+
final H2StreamChannel channel = mux.createChannel(1);
1095+
mux.createStream(channel, streamHandler);
1096+
1097+
final List<Header> trailers = Arrays.asList(
1098+
new BasicHeader(":status", "200"));
1099+
1100+
Assertions.assertThrows(H2ConnectionException.class, () -> channel.endStream(trailers));
1101+
}
10811102
private static byte[] encodeFrame(final RawFrame frame) throws IOException {
10821103
final WritableByteChannelMock writableChannel = new WritableByteChannelMock(256);
10831104
final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024);

httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamHandler.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,20 @@
2727
package org.apache.hc.core5.http2.impl.nio;
2828

2929
import java.nio.ByteBuffer;
30+
import java.util.Collections;
31+
import java.util.List;
3032

33+
import org.apache.hc.core5.http.Header;
3134
import org.apache.hc.core5.http.ProtocolException;
3235
import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
3336
import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
37+
import org.apache.hc.core5.http.message.BasicHeader;
3438
import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler;
3539
import org.apache.hc.core5.http.nio.AsyncPushConsumer;
3640
import org.apache.hc.core5.http.nio.HandlerFactory;
3741
import org.apache.hc.core5.http.protocol.HttpCoreContext;
3842
import org.apache.hc.core5.http.protocol.HttpProcessor;
43+
import org.apache.hc.core5.http2.H2ConnectionException;
3944
import org.junit.jupiter.api.Assertions;
4045
import org.junit.jupiter.api.Test;
4146
import org.mockito.Mockito;
@@ -101,4 +106,27 @@ void toStringIncludesStates() {
101106
Assertions.assertTrue(text.contains("responseState"));
102107
}
103108

109+
@Test
110+
void consumeTrailersWithPseudoHeaderRejected() throws Exception {
111+
final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class);
112+
final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
113+
final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics(
114+
new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics());
115+
final AsyncClientExchangeHandler exchangeHandler = Mockito.mock(AsyncClientExchangeHandler.class);
116+
@SuppressWarnings("unchecked") final HandlerFactory<AsyncPushConsumer> pushHandlerFactory =
117+
(HandlerFactory<AsyncPushConsumer>) Mockito.mock(HandlerFactory.class);
118+
final ClientH2StreamHandler handler = new ClientH2StreamHandler(
119+
channel, httpProcessor, metrics, exchangeHandler, pushHandlerFactory, HttpCoreContext.create());
120+
121+
final List<Header> responseHeaders = Collections.singletonList(
122+
new BasicHeader(":status", "200"));
123+
handler.consumeHeader(responseHeaders, false);
124+
125+
final List<Header> trailers = Collections.singletonList(
126+
new BasicHeader(":status", "200"));
127+
128+
Assertions.assertThrows(H2ConnectionException.class, () -> handler.consumeHeader(trailers, true));
129+
Mockito.verify(exchangeHandler, Mockito.never()).streamEnd(Mockito.anyList());
130+
}
131+
104132
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.core5.http2.impl.nio;
28+
29+
import java.util.Arrays;
30+
import java.util.Collections;
31+
32+
import org.apache.hc.core5.http.message.BasicHeader;
33+
import org.apache.hc.core5.http2.H2ConnectionException;
34+
import org.junit.jupiter.api.Assertions;
35+
import org.junit.jupiter.api.Test;
36+
37+
class TestTrailersValidationSupport {
38+
39+
@Test
40+
void testVerifyInboundAcceptsRegularTrailers() throws Exception {
41+
TrailersValidationSupport.verify(Arrays.asList(
42+
new BasicHeader("checksum", "abc"),
43+
new BasicHeader("x-foo", "bar")));
44+
}
45+
46+
@Test
47+
void testVerifyInboundRejectsPseudoHeaders() {
48+
Assertions.assertThrows(H2ConnectionException.class, () ->
49+
TrailersValidationSupport.verify(Collections.singletonList(
50+
new BasicHeader(":status", "200"))));
51+
}
52+
}

0 commit comments

Comments
 (0)