From 713a1853ed61886963d954564f809511fc54fc4d Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Mon, 16 Feb 2026 10:57:09 +0100 Subject: [PATCH] HTTP/2: validate Host vs :authority strictly --- .../http2/protocol/H2RequestValidateHost.java | 20 +++++++ .../protocol/TestH2RequestValidateHost.java | 60 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestValidateHost.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestValidateHost.java index c704e7bfb8..a492931020 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestValidateHost.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/protocol/H2RequestValidateHost.java @@ -32,12 +32,17 @@ import org.apache.hc.core5.annotation.Contract; 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.ProtocolException; import org.apache.hc.core5.http.ProtocolVersion; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.RequestValidateHost; +import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.TextUtils; /** * HTTP/2 compatible extension of {@link RequestValidateHost}. @@ -64,9 +69,24 @@ public void process( final EntityDetails entity, final HttpContext context) throws HttpException, IOException { Args.notNull(context, "HTTP context"); + Args.notNull(request, "HTTP request"); + final ProtocolVersion ver = context.getProtocolVersion(); if (ver.getMajor() < 2) { super.process(request, entity, context); + return; + } + + final URIAuthority authority = request.getAuthority(); + final Header hostHeader = request.getFirstHeader(HttpHeaders.HOST); + if (authority == null || hostHeader == null) { + return; + } + + final String hostValue = hostHeader.getValue(); + final String authorityValue = authority.toString(); + if (TextUtils.isBlank(hostValue) || !hostValue.equalsIgnoreCase(authorityValue)) { + throw new ProtocolException("Host header does not match :authority"); } } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java index cb4ec9cb43..e9a416ec88 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java @@ -26,12 +26,18 @@ */ package org.apache.hc.core5.http2.protocol; +import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.net.URIAuthority; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; class TestH2RequestValidateHost { @@ -59,4 +65,58 @@ void skipsValidationForHttp2() throws Exception { Assertions.assertNull(request.getAuthority()); } + @Test + void testHttp2HostMatchesAuthority() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.setScheme("https"); + request.setAuthority(new URIAuthority("example.com")); + request.addHeader(HttpHeaders.HOST, "example.com"); + + final HttpContext context = Mockito.mock(HttpContext.class); + Mockito.when(context.getProtocolVersion()).thenReturn(HttpVersion.HTTP_2); + + H2RequestValidateHost.INSTANCE.process(request, (EntityDetails) null, context); + } + + @Test + void testHttp2HostMismatchRejected() { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.setScheme("https"); + request.setAuthority(new URIAuthority("example.com")); + request.addHeader(HttpHeaders.HOST, "evil.com"); + + final HttpContext context = Mockito.mock(HttpContext.class); + Mockito.when(context.getProtocolVersion()).thenReturn(HttpVersion.HTTP_2); + + Assertions.assertThrows(ProtocolException.class, () -> + H2RequestValidateHost.INSTANCE.process(request, null, context)); + } + + @Test + void testHttp2HostCaseInsensitiveMatch() throws Exception { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.setScheme("https"); + request.setAuthority(new URIAuthority("example.com")); + request.addHeader(HttpHeaders.HOST, "EXAMPLE.COM"); + + final HttpContext context = Mockito.mock(HttpContext.class); + Mockito.when(context.getProtocolVersion()).thenReturn(HttpVersion.HTTP_2); + + H2RequestValidateHost.INSTANCE.process(request, null, context); + } + + @Test + void testHttp2HostPortDiffersRejected() { + final HttpRequest request = new BasicHttpRequest("GET", "/"); + request.setScheme("https"); + request.setAuthority(new URIAuthority("example.com", 443)); + request.addHeader(HttpHeaders.HOST, "example.com"); + + final HttpContext context = Mockito.mock(HttpContext.class); + Mockito.when(context.getProtocolVersion()).thenReturn(HttpVersion.HTTP_2); + + Assertions.assertThrows(ProtocolException.class, () -> + H2RequestValidateHost.INSTANCE.process(request, null, context)); + } + }