From 945c58793f405a1096baa462159e654916fdc4c1 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 13 Oct 2026 07:45:28 +0200 Subject: [PATCH] core: Do not offer 'h2' via Upgrade on TLS connections. * server/protocol.c (ap_get_protocol_upgrades): Exclude 'h2' from the list of protocols announced in the Upgrade response header when the connection is already using TLS. RFC 9113 section 3.3 requires HTTP/2 over TLS to be negotiated exclusively via ALPN; offering 'h2' through the HTTP/1.1 Upgrade mechanism on TLS is a protocol violation that some clients handle poorly. * test/modules/http2/test_103_upgrade.py (test_h2_103_06): Update to assert the corrected behaviour. Backport of trunk r1740075-equivalent fix (PR #836), restoring the 2016 fix that was reverted in r1748591 only for lack of a 2.4.x backport, not for being incorrect. PR: 59311 --- CHANGES | 4 ++++ server/protocol.c | 7 +++++++ test/modules/http2/test_103_upgrade.py | 6 +++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index c9d98b61da4..90e95959188 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,10 @@ -*- coding: utf-8 -*- Changes with Apache 2.4.70 + *) core: Do not offer 'h2' via the Upgrade response header on TLS + connections. RFC 9113 requires HTTP/2 over TLS to be negotiated + via ALPN only. PR 59311 + Changes with Apache 2.4.69 *) SECURITY: CVE-2026-93546: mod_dav_fs namespace overflow diff --git a/server/protocol.c b/server/protocol.c index 6fd11793552..7b303a9e001 100644 --- a/server/protocol.c +++ b/server/protocol.c @@ -42,6 +42,7 @@ #include "http_main.h" #include "http_request.h" #include "http_vhost.h" +#include "http_ssl.h" #include "http_log.h" /* For errors detected in basic auth common * support code... */ #include "mod_core.h" @@ -2428,6 +2429,12 @@ AP_DECLARE(apr_status_t) ap_get_protocol_upgrades(conn_rec *c, request_rec *r, sizeof(char *)); for (i = 0; i < conf->protocols->nelts; i++) { const char *p = APR_ARRAY_IDX(conf->protocols, i, char *); + /* RFC 9113, Section 3.3: HTTP/2 over TLS is negotiated + * exclusively via ALPN; 'h2' must never be offered through + * the HTTP/1.1 Upgrade mechanism on a TLS connection. */ + if (!strcmp("h2", p) && ap_ssl_conn_is_ssl(c)) { + continue; + } if (strcmp(existing, p)) { /* not the one we have and possible, add in this order */ APR_ARRAY_PUSH(upgrades, const char*) = p; diff --git a/test/modules/http2/test_103_upgrade.py b/test/modules/http2/test_103_upgrade.py index ec9a568b839..c5c7d47cabc 100644 --- a/test/modules/http2/test_103_upgrade.py +++ b/test/modules/http2/test_103_upgrade.py @@ -69,14 +69,14 @@ def test_h2_103_05(self, env): assert r.response assert "upgrade" not in r.response["header"] - # accessing https://test1, will advertise h2 in the response + # accessing https://test1, must not advertise h2 in the response: RFC 9113 + # section 3.3 requires HTTP/2 over TLS to be negotiated via ALPN only. def test_h2_103_06(self, env): url = env.mkurl("https", "test1", "/index.html") r = env.curl_get(url, options=["--http1.1"]) assert 0 == r.exit_code assert r.response - assert "upgrade" in r.response["header"] - assert "h2" == r.response["header"]["upgrade"] + assert "upgrade" not in r.response["header"] # accessing https://test3, will not send Upgrade since it is suppressed def test_h2_103_07(self, env):