diff --git a/SOURCES/0004-nghttp2-1.43.0-CVE-2026-58055.patch b/SOURCES/0004-nghttp2-1.43.0-CVE-2026-58055.patch new file mode 100644 index 0000000..b672c58 --- /dev/null +++ b/SOURCES/0004-nghttp2-1.43.0-CVE-2026-58055.patch @@ -0,0 +1,298 @@ +From 0d7b33e45e6559f6b12932b5b71a4cb18dbb8fcb Mon Sep 17 00:00:00 2001 +From: Tatsuhiro Tsujikawa +Date: Fri, 22 May 2026 21:26:44 +0900 +Subject: [PATCH] nghttpx: Tighten up CONNECT and HTTP Upgrade handling + +(cherry picked from commit ab28105c4a0197da24f8bfc414bc116055249e1e) +--- + src/shrpx_downstream.cc | 3 +- + src/shrpx_downstream.h | 7 ++- + src/shrpx_http2_upstream.cc | 8 +++ + src/shrpx_http_downstream_connection.cc | 68 +++++++++++++++++++++---- + src/shrpx_http_downstream_connection.h | 5 ++ + src/shrpx_https_upstream.cc | 36 +++++++++++-- + 6 files changed, 111 insertions(+), 16 deletions(-) + +diff --git a/src/shrpx_downstream.cc b/src/shrpx_downstream.cc +index dc7762d0..51810297 100644 +--- a/src/shrpx_downstream.cc ++++ b/src/shrpx_downstream.cc +@@ -1108,7 +1108,8 @@ bool Downstream::can_detach_downstream_connection() const { + // state, especially for HTTP/1.1 + return dconn_ && response_state_ == DownstreamState::MSG_COMPLETE && + request_state_ == DownstreamState::MSG_COMPLETE && !upgraded_ && +- !resp_.connection_close && request_buf_.rleft() == 0; ++ !resp_.connection_close && blocked_request_buf_.rleft() == 0 && ++ request_buf_.rleft() == 0; + } + + DefaultMemchunks Downstream::pop_response_buf() { +diff --git a/src/shrpx_downstream.h b/src/shrpx_downstream.h +index fff49a1a..0a479519 100644 +--- a/src/shrpx_downstream.h ++++ b/src/shrpx_downstream.h +@@ -160,7 +160,8 @@ struct Request { + connection_close(false), + http2_expect_body(false), + no_authority(false), +- forwarded_once(false) {} ++ forwarded_once(false), ++ http1_msg_complete(false) {} + + void consume(size_t len) { + assert(unconsumed_body_length >= len); +@@ -226,6 +227,10 @@ struct Request { + // orig_authority and orig_path have the authority and path which + // are used for the first backend selection. + bool forwarded_once; ++ // true if HTTP/1 request message has been completed. This field is ++ // added because Downstream::get_request_state() might be altered ++ // from DownstreamState::MSG_COMPLETE. ++ bool http1_msg_complete{}; + }; + + struct Response { +diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc +index 9b9e5523..406c713c 100644 +--- a/src/shrpx_http2_upstream.cc ++++ b/src/shrpx_http2_upstream.cc +@@ -359,6 +359,14 @@ int Http2Upstream::on_request_headers(Downstream *downstream, + return 0; + } + ++ if (method_token == HTTP_CONNECT && content_length) { ++ if (LOG_ENABLED(INFO)) { ++ ULOG(INFO, this) << "content-length are not allowed in CONNECT request"; ++ } ++ ++ return error_reply(downstream, 400); ++ } ++ + auto faddr = handler_->get_upstream_addr(); + + // For HTTP/2 proxy, we require :authority. +diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc +index f68cfcf5..0cecc1ce 100644 +--- a/src/shrpx_http_downstream_connection.cc ++++ b/src/shrpx_http_downstream_connection.cc +@@ -709,6 +709,34 @@ int HttpDownstreamConnection::push_request_headers() { + return 0; + } + ++bool HttpDownstreamConnection::should_block_request_body() const { ++ const auto &req = downstream_->request(); ++ ++ return !downstream_->get_request_header_sent() || ++ (req.upgrade_request && !downstream_->get_upgraded()); ++} ++ ++bool HttpDownstreamConnection::should_unblock_request_body_before_response() ++ const { ++ const auto &req = downstream_->request(); ++ ++ return !req.upgrade_request; ++} ++ ++void HttpDownstreamConnection::process_blocked_request_buf_on_response() { ++ if (blocked_request_buf_processed_) { ++ return; ++ } ++ ++ process_blocked_request_buf(); ++ ++ auto buf = downstream_->get_blocked_request_buf(); ++ buf->reset(); ++ blocked_request_buf_processed_ = true; ++ ++ signal_write(); ++} ++ + int HttpDownstreamConnection::process_blocked_request_buf() { + auto src = downstream_->get_blocked_request_buf(); + +@@ -738,7 +766,7 @@ int HttpDownstreamConnection::process_blocked_request_buf() { + + int HttpDownstreamConnection::push_upload_data_chunk(const uint8_t *data, + size_t datalen) { +- if (!downstream_->get_request_header_sent()) { ++ if (should_block_request_body()) { + auto output = downstream_->get_blocked_request_buf(); + auto &req = downstream_->request(); + output->append(data, datalen); +@@ -770,7 +798,7 @@ int HttpDownstreamConnection::push_upload_data_chunk(const uint8_t *data, + } + + int HttpDownstreamConnection::end_upload_data() { +- if (!downstream_->get_request_header_sent()) { ++ if (should_block_request_body()) { + downstream_->set_blocked_request_data_eof(true); + if (request_header_written_) { + signal_write(); +@@ -961,6 +989,11 @@ int htp_hdrs_completecb(llhttp_t *htp) { + // upgrade succeeded, 101 response is treated as final in nghttpx. + downstream->check_upgrade_fulfilled_http1(); + ++ if (req.method == HTTP_CONNECT && resp.http_status / 100 == 2 && ++ !downstream->get_upgraded()) { ++ resp.http_status = 502; ++ } ++ + if (downstream->get_non_final_response()) { + // Reset content-length because we reuse same Downstream for the + // next response. +@@ -987,13 +1020,22 @@ int htp_hdrs_completecb(llhttp_t *htp) { + resp.connection_close = true; + // transfer-encoding not applied to upgraded connection + downstream->set_chunked_response(false); +- } else if (http2::legacy_http1(req.http_major, req.http_minor)) { +- if (resp.fs.content_length == -1) { ++ ++ static_cast(dconn) ++ ->process_blocked_request_buf_on_response(); ++ } else { ++ if (req.upgrade_request) { + resp.connection_close = true; + } +- downstream->set_chunked_response(false); +- } else if (!downstream->expect_response_body()) { +- downstream->set_chunked_response(false); ++ ++ if (http2::legacy_http1(req.http_major, req.http_minor)) { ++ if (resp.fs.content_length == -1) { ++ resp.connection_close = true; ++ } ++ downstream->set_chunked_response(false); ++ } else if (!downstream->expect_response_body()) { ++ downstream->set_chunked_response(false); ++ } + } + + if (loggingconf.access.write_early && downstream->accesslog_ready()) { +@@ -1161,7 +1203,10 @@ int htp_msg_completecb(llhttp_t *htp) { + int HttpDownstreamConnection::write_first() { + int rv; + +- process_blocked_request_buf(); ++ auto should_unblock_req_body = should_unblock_request_body_before_response(); ++ if (should_unblock_req_body) { ++ process_blocked_request_buf(); ++ } + + if (conn_.tls.ssl) { + rv = write_tls(); +@@ -1182,8 +1227,11 @@ int HttpDownstreamConnection::write_first() { + first_write_done_ = true; + downstream_->set_request_header_sent(true); + +- auto buf = downstream_->get_blocked_request_buf(); +- buf->reset(); ++ if (should_unblock_req_body) { ++ auto buf = downstream_->get_blocked_request_buf(); ++ buf->reset(); ++ blocked_request_buf_processed_ = true; ++ } + + // upstream->resume_read() might be called in + // write_tls()/write_clear(), but before blocked_request_buf_ is +diff --git a/src/shrpx_http_downstream_connection.h b/src/shrpx_http_downstream_connection.h +index a453f0dc..f5fcb21f 100644 +--- a/src/shrpx_http_downstream_connection.h ++++ b/src/shrpx_http_downstream_connection.h +@@ -91,6 +91,9 @@ public: + int noop(); + + int process_blocked_request_buf(); ++ void process_blocked_request_buf_on_response(); ++ bool should_unblock_request_body_before_response() const; ++ bool should_block_request_body() const; + + private: + Connection conn_; +@@ -117,6 +120,8 @@ private: + bool reusable_; + // true if request header is written to request buffer. + bool request_header_written_; ++ // true if blocked request buffer has been processed. ++ bool blocked_request_buf_processed_{}; + }; + + } // namespace shrpx +diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc +index 34a5504b..f69c57cd 100644 +--- a/src/shrpx_https_upstream.cc ++++ b/src/shrpx_https_upstream.cc +@@ -396,6 +396,17 @@ int htp_hdrs_completecb(llhttp_t *htp) { + + downstream->inspect_http1_request(); + ++ if ((req.upgrade_request || htp->upgrade) && ++ (req.fs.header(http2::HD_TRANSFER_ENCODING) || ++ req.fs.header(http2::HD_CONTENT_LENGTH))) { ++ if (LOG_ENABLED(INFO)) { ++ ULOG(INFO, upstream) << "transfer-encoding and content-length are not " ++ "allowed in CONNECT or upgrade request"; ++ } ++ ++ return -1; ++ } ++ + auto faddr = handler->get_upstream_addr(); + auto &balloc = downstream->get_block_allocator(); + auto config = get_config(); +@@ -522,6 +533,16 @@ int htp_bodycb(llhttp_t *htp, const char *data, size_t len) { + int rv; + auto upstream = static_cast(htp->data); + auto downstream = upstream->get_downstream(); ++ const auto &req = downstream->request(); ++ ++ if (req.upgrade_request || htp->upgrade) { ++ if (LOG_ENABLED(INFO)) { ++ ULOG(INFO, upstream) << "Request body for Upgrade request is not allowed"; ++ } ++ ++ return HPE_USER; ++ } ++ + rv = downstream->push_upload_data_chunk( + reinterpret_cast(data), len); + if (rv != 0) { +@@ -547,7 +568,9 @@ int htp_msg_completecb(llhttp_t *htp) { + } + auto handler = upstream->get_client_handler(); + auto downstream = upstream->get_downstream(); ++ auto &req = downstream->request(); + downstream->set_request_state(DownstreamState::MSG_COMPLETE); ++ req.http1_msg_complete = true; + rv = downstream->end_upload_data(); + if (rv != 0) { + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { +@@ -589,7 +612,8 @@ int HttpsUpstream::on_read() { + + // downstream can be nullptr here, because it is initialized in the + // callback chain called by llhttp_execute() +- if (downstream && downstream->get_upgraded()) { ++ if (downstream && downstream->request().http1_msg_complete && ++ downstream->get_upgraded()) { + + auto rv = downstream->push_upload_data_chunk(rb->pos(), rb->rleft()); + +@@ -655,9 +679,13 @@ int HttpsUpstream::on_read() { + + if (htperr != HPE_OK) { + if (LOG_ENABLED(INFO)) { +- ULOG(INFO, this) << "HTTP parse failure: " +- << "(" << llhttp_errno_name(htperr) << ") " +- << llhttp_get_error_reason(&htp_); ++ if (htperr == HPE_USER) { ++ ULOG(INFO, this) << "HTTP callback error"; ++ } else { ++ ULOG(INFO, this) << "HTTP parse failure: " ++ << "(" << llhttp_errno_name(htperr) << ") " ++ << llhttp_get_error_reason(&htp_); ++ } + } + + if (downstream && +-- +2.55.0 + diff --git a/SPECS/nghttp2.spec b/SPECS/nghttp2.spec index 14cedd4..8adb15e 100644 --- a/SPECS/nghttp2.spec +++ b/SPECS/nghttp2.spec @@ -1,7 +1,7 @@ Summary: Experimental HTTP/2 client, server and proxy Name: nghttp2 Version: 1.43.0 -Release: 6%{?dist}.1 +Release: 6%{?dist}.2 License: MIT URL: https://nghttp2.org/ Source0: https://github.com/tatsuhiro-t/nghttp2/releases/download/v%{version}/nghttp2-%{version}.tar.xz @@ -15,6 +15,9 @@ Patch2: 0002-nghttp2-1.43.0-CVE-2024-28182-CVE-2024-27316.patch # fix Denial of service: Assertion failure due to the missing state validation (CVE-2026-27135) Patch3: 0003-nghttp2-1.43.0-CVE-2026-27135.patch +# fix HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests (CVE-2026-58055) +Patch4: 0004-nghttp2-1.43.0-CVE-2026-58055.patch + BuildRequires: automake BuildRequires: libtool @@ -128,6 +131,9 @@ export "LD_LIBRARY_PATH=$RPM_BUILD_ROOT%{_libdir}:$LD_LIBRARY_PATH" %changelog +* Wed Aug 12 2026 Jan Macku - 1.43.0-6.2 +- fix HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests (CVE-2026-58055) + * Wed Apr 01 2026 Jan Macku - 1.43.0-6.1 - fix Denial of service: Assertion failure due to the missing state validation (CVE-2026-27135)