diff --git a/0003-nghttp2-1.68.0-CVE-2026-58055.patch b/0003-nghttp2-1.68.0-CVE-2026-58055.patch new file mode 100644 index 0000000..3533cd3 --- /dev/null +++ b/0003-nghttp2-1.68.0-CVE-2026-58055.patch @@ -0,0 +1,315 @@ +From 455624dc9f1c5f59305f46c2f04b96f0acb0e4b1 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 | 4 ++ + src/shrpx_http2_upstream.cc | 8 +++ + src/shrpx_http3_upstream.cc | 8 +++ + src/shrpx_http_downstream_connection.cc | 70 +++++++++++++++++++++---- + src/shrpx_http_downstream_connection.h | 5 ++ + src/shrpx_https_upstream.cc | 35 +++++++++++-- + 7 files changed, 117 insertions(+), 16 deletions(-) + +diff --git a/src/shrpx_downstream.cc b/src/shrpx_downstream.cc +index 9cffcd7c..9f08e831 100644 +--- a/src/shrpx_downstream.cc ++++ b/src/shrpx_downstream.cc +@@ -1148,7 +1148,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 f87c7f49..42dd42af 100644 +--- a/src/shrpx_downstream.h ++++ b/src/shrpx_downstream.h +@@ -233,6 +233,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 3c3ca0f5..10fdf10b 100644 +--- a/src/shrpx_http2_upstream.cc ++++ b/src/shrpx_http2_upstream.cc +@@ -330,6 +330,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_http3_upstream.cc b/src/shrpx_http3_upstream.cc +index e79fb3fa..c5c7cb9f 100644 +--- a/src/shrpx_http3_upstream.cc ++++ b/src/shrpx_http3_upstream.cc +@@ -2256,6 +2256,14 @@ int Http3Upstream::http_end_request_headers(Downstream *downstream, int fin) { + 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(); + + auto config = get_config(); +diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc +index fb9f201c..4308d926 100644 +--- a/src/shrpx_http_downstream_connection.cc ++++ b/src/shrpx_http_downstream_connection.cc +@@ -715,6 +715,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(); + +@@ -744,7 +772,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); +@@ -776,7 +804,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(); +@@ -977,6 +1005,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. +@@ -998,7 +1031,7 @@ int htp_hdrs_completecb(llhttp_t *htp) { + downstream->set_response_state(DownstreamState::HEADER_COMPLETE); + downstream->inspect_http1_response(); + +- if (htp->flags & F_CHUNKED) { ++ if (!downstream->get_upgraded() && (htp->flags & F_CHUNKED)) { + downstream->set_chunked_response(true); + } + +@@ -1013,13 +1046,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()) { +@@ -1197,7 +1239,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(); +@@ -1218,8 +1263,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 b9f70c74..2f1a1a1c 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 5de140fb..4847c8e2 100644 +--- a/src/shrpx_https_upstream.cc ++++ b/src/shrpx_https_upstream.cc +@@ -408,6 +408,17 @@ int htp_hdrs_completecb(llhttp_t *htp) { + + downstream->inspect_http1_request(); + ++ if ((req.upgrade_request || llhttp_get_upgrade(htp)) && ++ (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; ++ } ++ + if (htp->flags & F_CHUNKED) { + downstream->set_chunked_request(true); + } +@@ -552,6 +563,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 || llhttp_get_upgrade(htp)) { ++ 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) { +@@ -585,6 +606,7 @@ int htp_msg_completecb(llhttp_t *htp) { + } + + 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) { +@@ -625,7 +647,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()); + + if (rv != 0) { +@@ -698,9 +721,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/nghttp2.spec b/nghttp2.spec index 99ad3c6..45e1591 100644 --- a/nghttp2.spec +++ b/nghttp2.spec @@ -7,7 +7,7 @@ Summary: Experimental HTTP/2 client, server and proxy Name: nghttp2 Version: 1.68.0 -Release: 3%{?dist}.1 +Release: 3%{?dist}.2 # Parts of ruby bindings are additionally under GPL-2.0-or-later, MIT and # HPND-Kevlin-Henney but they are NOT shipped. @@ -22,6 +22,9 @@ Source2: tatsuhiro-t.pgp Patch001: 0001-nghttp2-1.68.0-Check-nghttp2_is_fatal-first.patch Patch002: 0002-nghttp2-1.68.0-Fix-missing-iframe-state-validations-to-avoid-assert.patch +# fix HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests (CVE-2026-58055) +Patch003: 0003-nghttp2-1.68.0-CVE-2026-58055.patch + # Make X25519MLKEM768 the default TLS key exchange group in nghttpd and nghttpx # https://issues.redhat.com/browse/RHEL-103655 Patch100: 0100-nghttp2-1.64.0-pqc-add-X25519MLKEM768-as-the-default-TLS-key-exchan.patch @@ -217,6 +220,9 @@ popd %changelog +* Mon Aug 10 2026 Jan Macku 1.68.0-3.2 +- fix HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests (CVE-2026-58055) + * Tue Mar 31 2026 Jan Macku 1.68.0-3.1 - fix Denial of service: Assertion failure due to the missing state validation (CVE-2026-27135)