From d2d6baaae8d6ae465e49d8afd659eea678053718 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Mon, 23 Feb 2026 11:13:30 +0100 Subject: [PATCH] TTP/2: validate pseudo-headers in inbound request trailers ServerH2StreamHandler forwarded inbound request trailers (HEADERS in BODY state) directly to AsyncServerExchangeHandler.streamEnd(...) without validating trailer semantics. This allowed pseudo-header fields (':...') to slip through on the server path. --- .../http2/impl/nio/ServerH2StreamHandler.java | 11 ++- .../impl/nio/TestServerH2StreamHandler.java | 67 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java index 55316d0a94..2313d2ec91 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ServerH2StreamHandler.java @@ -265,7 +265,16 @@ public void consumeHeader(final List
headers, final boolean endStream) t } break; case BODY: - responseState.set(MessageState.COMPLETE); + if (!endStream) { + throw new H2StreamResetException(H2Error.PROTOCOL_ERROR, "Trailer headers must set END_STREAM"); + } + try { + TrailersValidationSupport.verify(headers); + } catch (final ProtocolException ex) { + throw new H2StreamResetException(H2Error.PROTOCOL_ERROR, ex.getMessage()); + } + requestState.set(MessageState.COMPLETE); + Asserts.notNull(exchangeHandler, "Exchange handler"); exchangeHandler.streamEnd(headers); break; default: diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java index cf3f7c5a1a..6d4e71f96e 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java @@ -27,16 +27,24 @@ package org.apache.hc.core5.http2.impl.nio; import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.HandlerFactory; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpProcessor; +import org.apache.hc.core5.http2.H2Error; +import org.apache.hc.core5.http2.H2StreamResetException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; class TestServerH2StreamHandler { @@ -81,4 +89,63 @@ void toStringIncludesStates() { Assertions.assertTrue(text.contains("responseState")); } + @Test + void consumeTrailersWithPseudoHeaderRejected() throws Exception { + final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class); + final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class); + final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics( + new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics()); + final AsyncServerExchangeHandler exchangeHandler = Mockito.mock(AsyncServerExchangeHandler.class); + @SuppressWarnings("unchecked") + final HandlerFactory exchangeHandlerFactory = + (HandlerFactory) Mockito.mock(HandlerFactory.class); + Mockito.when(exchangeHandlerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(exchangeHandler); + + final ServerH2StreamHandler handler = new ServerH2StreamHandler( + channel, httpProcessor, metrics, exchangeHandlerFactory, HttpCoreContext.create()); + + final List
requestHeaders = Arrays.asList( + new BasicHeader(":method", "POST"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.test"), + new BasicHeader(":path", "/upload")); + handler.consumeHeader(requestHeaders, false); + + final List
trailers = Collections.singletonList(new BasicHeader(":status", "200")); + Assertions.assertThrows(H2StreamResetException.class, () -> handler.consumeHeader(trailers, true)); + Mockito.verify(exchangeHandler, Mockito.never()).streamEnd(ArgumentMatchers.anyList()); + } + + @Test + void consumeTrailersWithoutEndStreamRejected() throws Exception { + final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class); + final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class); + final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics( + new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics()); + final AsyncServerExchangeHandler exchangeHandler = Mockito.mock(AsyncServerExchangeHandler.class); + @SuppressWarnings("unchecked") + final HandlerFactory exchangeHandlerFactory = + (HandlerFactory) Mockito.mock(HandlerFactory.class); + Mockito.when(exchangeHandlerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(exchangeHandler); + + final ServerH2StreamHandler handler = new ServerH2StreamHandler( + channel, httpProcessor, metrics, exchangeHandlerFactory, HttpCoreContext.create()); + + final List
requestHeaders = Arrays.asList( + new BasicHeader(":method", "POST"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.test"), + new BasicHeader(":path", "/upload")); + handler.consumeHeader(requestHeaders, false); + + final List
trailers = Collections.singletonList(new BasicHeader("x-checksum", "abc123")); + final H2StreamResetException ex = Assertions.assertThrows( + H2StreamResetException.class, + () -> handler.consumeHeader(trailers, false)); + Assertions.assertEquals(H2Error.PROTOCOL_ERROR, H2Error.getByCode(ex.getCode())); + Mockito.verify(exchangeHandler, Mockito.never()).streamEnd(ArgumentMatchers.anyList()); + } + }