Skip to content

Commit ab911ae

Browse files
authored
Trailers must not include pseudo-header fields (#3810)
https://www.rfc-editor.org/rfc/rfc9113.html#name-http-message-framing Signed-off-by: Violeta Georgieva <[email protected]>
1 parent e8981a7 commit ab911ae

File tree

4 files changed

+85
-11
lines changed

4 files changed

+85
-11
lines changed

reactor-netty-http/src/http3Test/java/reactor/netty/http/Http3Tests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,20 @@ void testTrailerHeadersNotSpecifiedUpfront() {
964964
doTestTrailerHeaders(createClient(disposableServer.port()), "empty", "testTrailerHeadersNotSpecifiedUpfront");
965965
}
966966

967+
@Test
968+
void testTrailerHeadersPseudoHeaderNotAllowed() {
969+
disposableServer =
970+
createServer()
971+
.handle((req, res) ->
972+
res.header(HttpHeaderNames.TRAILER, ":protocol")
973+
.trailerHeaders(h -> h.set(":protocol", "test"))
974+
.sendString(Flux.just("testTrailerHeaders", "PseudoHeaderNotAllowed")))
975+
.bindNow();
976+
977+
// Trailers MUST NOT include pseudo-header fields
978+
doTestTrailerHeaders(createClient(disposableServer.port()), "empty", "testTrailerHeadersPseudoHeaderNotAllowed");
979+
}
980+
967981
private static void doTestTrailerHeaders(HttpClient client, String expectedHeaderValue, String expectedResponse) {
968982
client.get()
969983
.uri("/")

reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
7575
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
7676
import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
77+
import io.netty.handler.codec.http2.Http2Headers;
7778
import io.netty.handler.timeout.ReadTimeoutHandler;
7879
import io.netty.util.AsciiString;
7980
import io.netty.util.ReferenceCountUtil;
@@ -1000,6 +1001,7 @@ else if (markSentBody()) {
10001001
}
10011002

10021003
@Nullable
1004+
@SuppressWarnings("ReferenceEquality")
10031005
private HttpHeaders prepareTrailerHeaders() {
10041006
HttpHeaders trailerHeaders = null;
10051007
// https://datatracker.ietf.org/doc/html/rfc7230#section-4.1.2
@@ -1009,7 +1011,8 @@ private HttpHeaders prepareTrailerHeaders() {
10091011
// message integrity check, digital signature, or post-processing
10101012
// status.
10111013
// There is no requirement for chunked message when HTTP/2 and HTTP/3
1012-
if (trailerHeadersConsumer != null && (version() != HttpVersion.HTTP_1_1 || isTransferEncodingChunked(nettyResponse))) {
1014+
boolean isNotHttp11 = version() != HttpVersion.HTTP_1_1;
1015+
if (trailerHeadersConsumer != null && (isNotHttp11 || isTransferEncodingChunked(nettyResponse))) {
10131016
// https://datatracker.ietf.org/doc/html/rfc7230#section-4.4
10141017
// When a message includes a message body encoded with the chunked
10151018
// transfer coding and the sender desires to send metadata in the form
@@ -1018,7 +1021,7 @@ private HttpHeaders prepareTrailerHeaders() {
10181021
// which fields will be present in the trailers.
10191022
String declaredHeaderNames = responseHeaders.get(HttpHeaderNames.TRAILER);
10201023
if (declaredHeaderNames != null) {
1021-
trailerHeaders = new TrailerHeaders(declaredHeaderNames);
1024+
trailerHeaders = new TrailerHeaders(declaredHeaderNames, isNotHttp11);
10221025
try {
10231026
trailerHeadersConsumer.accept(trailerHeaders);
10241027
}
@@ -1437,8 +1440,8 @@ static final class TrailerHeaders extends DefaultHttpHeaders {
14371440
DISALLOWED_TRAILER_HEADER_NAMES.add("warning");
14381441
}
14391442

1440-
TrailerHeaders(String declaredHeaderNames) {
1441-
super(true, new TrailerNameValidator(filterHeaderNames(declaredHeaderNames)));
1443+
TrailerHeaders(String declaredHeaderNames, boolean isNotHttp11) {
1444+
super(true, new TrailerNameValidator(filterHeaderNames(declaredHeaderNames), isNotHttp11));
14421445
}
14431446

14441447
static Set<String> filterHeaderNames(String declaredHeaderNames) {
@@ -1462,9 +1465,11 @@ static final class TrailerNameValidator implements DefaultHeaders.NameValidator<
14621465
* Contains the headers names specified with {@link HttpHeaderNames#TRAILER}.
14631466
*/
14641467
final Set<String> declaredHeaderNames;
1468+
final boolean isNotHttp11;
14651469

1466-
TrailerNameValidator(Set<String> declaredHeaderNames) {
1470+
TrailerNameValidator(Set<String> declaredHeaderNames, boolean isNotHttp11) {
14671471
this.declaredHeaderNames = declaredHeaderNames;
1472+
this.isNotHttp11 = isNotHttp11;
14681473
}
14691474

14701475
@Override
@@ -1473,6 +1478,12 @@ public void validateName(CharSequence name) {
14731478
throw new IllegalArgumentException("Trailer header name [" + name +
14741479
"] not declared with [Trailer] header, or it is not a valid trailer header name");
14751480
}
1481+
// https://www.rfc-editor.org/rfc/rfc9113.html#name-http-message-framing
1482+
// Trailers MUST NOT include pseudo-header fields
1483+
else if (isNotHttp11 && Http2Headers.PseudoHeaderName.hasPseudoHeaderFormat(name)) {
1484+
throw new IllegalArgumentException("Pseudo header name [" + name +
1485+
"] found in trailer headers, trailer headers cannot have pseudo headers");
1486+
}
14761487
}
14771488
}
14781489
}

reactor-netty-http/src/test/java/reactor/netty/http/Http2Tests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package reactor.netty.http;
1717

1818
import io.netty.buffer.Unpooled;
19+
import io.netty.handler.codec.http.HttpHeaderNames;
1920
import io.netty.handler.codec.http2.Http2Connection;
2021
import io.netty.handler.codec.http2.Http2FrameCodec;
2122
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
@@ -730,6 +731,46 @@ void h2ClientSendsError(HttpProtocol[] serverProtocols, HttpProtocol[] clientPro
730731
}
731732
}
732733

734+
@ParameterizedTest
735+
@MethodSource("h2CompatibleCombinations")
736+
@SuppressWarnings("deprecation")
737+
void testTrailerHeadersPseudoHeaderNotAllowedH2(HttpProtocol[] serverProtocols, HttpProtocol[] clientProtocols) {
738+
Http2SslContextSpec serverCtx = Http2SslContextSpec.forServer(ssc.certificate(), ssc.privateKey());
739+
Http2SslContextSpec clientCtx =
740+
Http2SslContextSpec.forClient()
741+
.configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE));
742+
testTrailerHeadersPseudoHeaderNotAllowed(
743+
createServer().protocol(serverProtocols).secure(spec -> spec.sslContext(serverCtx)),
744+
createClient(() -> disposableServer.address()).protocol(clientProtocols).secure(spec -> spec.sslContext(clientCtx)));
745+
}
746+
747+
@ParameterizedTest
748+
@MethodSource("h2cCompatibleCombinations")
749+
void testTrailerHeadersPseudoHeaderNotAllowedH2C(HttpProtocol[] serverProtocols, HttpProtocol[] clientProtocols) {
750+
testTrailerHeadersPseudoHeaderNotAllowed(
751+
createServer().protocol(serverProtocols),
752+
createClient(() -> disposableServer.address()).protocol(clientProtocols));
753+
}
754+
755+
private void testTrailerHeadersPseudoHeaderNotAllowed(HttpServer server, HttpClient client) {
756+
disposableServer =
757+
server.handle((req, res) ->
758+
res.header(HttpHeaderNames.TRAILER, ":protocol")
759+
.trailerHeaders(h -> h.set(":protocol", "test"))
760+
.sendString(Flux.just("testTrailerHeaders", "PseudoHeaderNotAllowed")))
761+
.bindNow();
762+
763+
// Trailers MUST NOT include pseudo-header fields
764+
client.get()
765+
.uri("/")
766+
.responseSingle((res, bytes) -> bytes.asString().defaultIfEmpty("empty").zipWith(res.trailerHeaders()))
767+
.as(StepVerifier::create)
768+
.expectNextMatches(t -> "testTrailerHeadersPseudoHeaderNotAllowed".equals(t.getT1()) &&
769+
"empty".equals(t.getT2().get(":protocol", "empty")))
770+
.expectComplete()
771+
.verify(Duration.ofSeconds(5));
772+
}
773+
733774
private void http2ClientSendsError(HttpServer server, HttpClient client) {
734775
disposableServer =
735776
server.http2Settings(spec -> spec.maxConcurrentStreams(1))

reactor-netty-http/src/test/java/reactor/netty/http/server/TrailerHeadersTests.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021-2024 VMware, Inc. or its affiliates, All Rights Reserved.
2+
* Copyright (c) 2021-2025 VMware, Inc. or its affiliates, All Rights Reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,13 +40,14 @@ class TrailerHeadersTests {
4040
static final String HEADER_NAME_1 = "foo";
4141
static final String HEADER_NAME_2 = "bar";
4242
static final String HEADER_VALUE = "test";
43+
static final String PSEUDO_HEADER_NAME = ":protocol";
4344
static final String SPACE = " ";
4445

4546
@ParameterizedTest
4647
@MethodSource("disallowedTrailerHeaderNames")
4748
void testDisallowedTrailerHeaderNames(String declaredHeaderName) {
4849
assertThatExceptionOfType(IllegalArgumentException.class)
49-
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(declaredHeaderName).add(declaredHeaderName, HEADER_VALUE))
50+
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(declaredHeaderName, false).add(declaredHeaderName, HEADER_VALUE))
5051
.withMessage(String.format(ERROR_MESSAGE, declaredHeaderName));
5152
}
5253

@@ -59,7 +60,7 @@ void testDisallowedTrailerHeaderNames(String declaredHeaderName) {
5960
HEADER_NAME_1 + COMMA + SPACE
6061
})
6162
void testNameIncludedInTrailerHeader(String declaredHeaderNames) {
62-
HttpHeaders headers = new HttpServerOperations.TrailerHeaders(declaredHeaderNames);
63+
HttpHeaders headers = new HttpServerOperations.TrailerHeaders(declaredHeaderNames, false);
6364
assertThat(headers.isEmpty()).isTrue();
6465
headers.add(HEADER_NAME_1, HEADER_VALUE);
6566
assertThat(headers.isEmpty()).isFalse();
@@ -69,7 +70,7 @@ void testNameIncludedInTrailerHeader(String declaredHeaderNames) {
6970

7071
@Test
7172
void testNamesIncludedInTrailerHeader() {
72-
HttpHeaders headers = new HttpServerOperations.TrailerHeaders(HEADER_NAME_1 + ',' + HEADER_NAME_2);
73+
HttpHeaders headers = new HttpServerOperations.TrailerHeaders(HEADER_NAME_1 + ',' + HEADER_NAME_2, false);
7374
assertThat(headers.isEmpty()).isTrue();
7475
headers.add(HEADER_NAME_1, HEADER_VALUE);
7576
headers.add(HEADER_NAME_2, HEADER_VALUE);
@@ -82,18 +83,25 @@ void testNamesIncludedInTrailerHeader() {
8283
@Test
8384
void testNameNotIncludedInTrailerHeader() {
8485
assertThatExceptionOfType(IllegalArgumentException.class)
85-
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(HEADER_NAME_1).add(HEADER_NAME_2, HEADER_VALUE))
86+
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(HEADER_NAME_1, false).add(HEADER_NAME_2, HEADER_VALUE))
8687
.withMessage(String.format(ERROR_MESSAGE, HEADER_NAME_2));
8788
}
8889

8990
@ParameterizedTest
9091
@ValueSource(strings = {COMMA, EMPTY, SPACE})
9192
void testNothingIsIncludedInTrailerHeader(String declaredHeaderNames) {
9293
assertThatExceptionOfType(IllegalArgumentException.class)
93-
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(declaredHeaderNames).add(EMPTY, HEADER_VALUE))
94+
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(declaredHeaderNames, false).add(EMPTY, HEADER_VALUE))
9495
.withMessage(String.format(ERROR_MESSAGE, EMPTY));
9596
}
9697

98+
@Test
99+
void testPseudoHeaderInTrailerHeaderNames() {
100+
assertThatExceptionOfType(IllegalArgumentException.class)
101+
.isThrownBy(() -> new HttpServerOperations.TrailerHeaders(PSEUDO_HEADER_NAME, true).add(PSEUDO_HEADER_NAME, HEADER_VALUE))
102+
.withMessage("Pseudo header name [:protocol] found in trailer headers, trailer headers cannot have pseudo headers");
103+
}
104+
97105
static Set<String> disallowedTrailerHeaderNames() {
98106
return HttpServerOperations.TrailerHeaders.DISALLOWED_TRAILER_HEADER_NAMES;
99107
}

0 commit comments

Comments
 (0)