From fee002254ae218c76b5bce00aea1349b2b301ce1 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Mon, 16 Feb 2026 11:58:35 +0100 Subject: [PATCH] Validate all TE header instances for HTTP/2 request conformance. Reject any TE value other than trailers (including additional TE headers). --- .../http2/protocol/H2RequestConformance.java | 29 +++-- .../protocol/TestH2RequestConformance.java | 123 ++++++++++++++++++ 2 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java index ef528fbc29..f72fef2fb9 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestConformance.java @@ -28,17 +28,18 @@ package org.apache.hc.core5.http2.protocol; import java.io.IOException; +import java.util.Iterator; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.message.MessageSupport; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.util.Args; @@ -77,13 +78,9 @@ public void process(final HttpRequest request, final EntityDetails entity, final Args.notNull(request, "HTTP request"); for (int i = 0; i < illegalHeaderNames.length; i++) { final String headerName = illegalHeaderNames[i]; - final Header header = request.getFirstHeader(headerName); - if (header != null) { - if (headerName.equalsIgnoreCase(HttpHeaders.TE)) { - final String value = header.getValue(); - if (!"trailers".equalsIgnoreCase(value)) { - throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", HttpHeaders.TE, value); - } + if (request.containsHeader(headerName)) { + if (HttpHeaders.TE.equalsIgnoreCase(headerName)) { + validateTE(request); } else { throw new ProtocolException("Header '%s' is illegal for HTTP/2 messages", headerName); } @@ -91,4 +88,20 @@ public void process(final HttpRequest request, final EntityDetails entity, final } } + private static void validateTE(final HttpRequest request) throws ProtocolException { + boolean sawAnyToken = false; + boolean sawInvalidToken = false; + for (final Iterator it = MessageSupport.iterateTokens(request, HttpHeaders.TE); it.hasNext(); ) { + final String token = it.next(); + sawAnyToken = true; + if (!"trailers".equalsIgnoreCase(token)) { + sawInvalidToken = true; + break; + } + } + if (sawInvalidToken || !sawAnyToken) { + throw new ProtocolException("Header '%s' is illegal for HTTP/2 messages", HttpHeaders.TE); + } + } + } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java new file mode 100644 index 0000000000..01fac8c600 --- /dev/null +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConformance.java @@ -0,0 +1,123 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http2.protocol; + +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TestH2RequestConformance { + + @Test + void testTEAbsent() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + + @Test + void testTESingleTrailers() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers"); + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + + @Test + void testTECombinedTokensRejected() { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers, gzip"); + + Assertions.assertThrows(ProtocolException.class, + () -> H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create())); + } + + @Test + void testTEMultipleHeadersSecondIllegalRejected() { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers"); + request.addHeader(HttpHeaders.TE, "gzip"); + + Assertions.assertThrows(ProtocolException.class, + () -> H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create())); + } + + @Test + void testTEMultipleHeadersAllTrailersAccepted() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers"); + request.addHeader(HttpHeaders.TE, "trailers"); + + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + + @Test + void testTEWhitespaceAccepted() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, " trailers \t"); + + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + + @Test + void testTESingleTrailersAllowed() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers"); + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + + @Test + void testTECommaListRejected() { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers, deflate;q=0.5"); + + Assertions.assertThrows(ProtocolException.class, + () -> H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create())); + } + + + @Test + void testTEMultipleHeadersAllTrailersAllowed() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, "trailers"); + request.addHeader(HttpHeaders.TE, "trailers"); + + H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create()); + } + + + @Test + void testTEEmptyValueRejected() { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.addHeader(HttpHeaders.TE, ""); + + Assertions.assertThrows(ProtocolException.class, + () -> H2RequestConformance.INSTANCE.process(request, null, HttpCoreContext.create())); + } +}