CVE-2026-58055 - HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests

Resolves: RHEL-219140
This commit is contained in:
Jan Macku 2026-08-07 12:56:11 +02:00
parent bd3193b3c8
commit 977525a3eb
2 changed files with 329 additions and 1 deletions

View File

@ -0,0 +1,321 @@
From eda33c2454fc05f43394aa7624b6b19b6f712f03 Mon Sep 17 00:00:00 2001
From: Tatsuhiro Tsujikawa <tatsuhiro.t@gmail.com>
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 | 7 +-
src/shrpx_downstream.h | 8 ++-
src/shrpx_http2_upstream.cc | 8 +++
src/shrpx_http_downstream_connection.cc | 85 ++++++++++++++++++++++++-
src/shrpx_http_downstream_connection.h | 5 ++
src/shrpx_https_upstream.cc | 26 +++++++-
6 files changed, 133 insertions(+), 6 deletions(-)
diff --git a/src/shrpx_downstream.cc b/src/shrpx_downstream.cc
index b561d2ff..e6cff150 100644
--- a/src/shrpx_downstream.cc
+++ b/src/shrpx_downstream.cc
@@ -1058,7 +1058,8 @@ bool Downstream::can_detach_downstream_connection() const {
// state, especially for HTTP/1.1
return dconn_ && response_state_ == Downstream::MSG_COMPLETE &&
request_state_ == Downstream::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() {
@@ -1109,6 +1110,10 @@ bool Downstream::get_blocked_request_data_eof() const {
return blocked_request_data_eof_;
}
+void Downstream::set_blocked_request_data_eof(bool f) {
+ blocked_request_data_eof_ = f;
+}
+
bool Downstream::get_expect_100_continue() const {
return expect_100_continue_;
}
diff --git a/src/shrpx_downstream.h b/src/shrpx_downstream.h
index 1b12ebcd..02e63882 100644
--- a/src/shrpx_downstream.h
+++ b/src/shrpx_downstream.h
@@ -146,7 +146,8 @@ struct Request {
http2_upgrade_seen(false),
connection_close(false),
http2_expect_body(false),
- no_authority(false) {}
+ no_authority(false),
+ http1_msg_complete(false) {}
void consume(size_t len) {
assert(unconsumed_body_length >= len);
@@ -190,6 +191,10 @@ struct Request {
// This happens when: For HTTP/2 request, :authority is missing.
// For HTTP/1 request, origin or asterisk form is used.
bool no_authority;
+ // true if HTTP/1 request message has been completed. This field is
+ // added because Downstream::get_request_state() might be altered
+ // from Downstream::MSG_COMPLETE.
+ bool http1_msg_complete;
};
struct Response {
@@ -358,6 +363,7 @@ public:
DefaultMemchunks *get_blocked_request_buf();
bool get_blocked_request_data_eof() const;
+ void set_blocked_request_data_eof(bool f);
// downstream response API
const Response &response() const { return resp_; }
diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc
index fdf988cc..4a11775e 100644
--- a/src/shrpx_http2_upstream.cc
+++ b/src/shrpx_http2_upstream.cc
@@ -355,6 +355,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 47366b31..5deab304 100644
--- a/src/shrpx_http_downstream_connection.cc
+++ b/src/shrpx_http_downstream_connection.cc
@@ -206,7 +206,8 @@ HttpDownstreamConnection::HttpDownstreamConnection(
response_htp_{0},
initial_addr_idx_(initial_addr_idx),
reuse_first_write_done_(true),
- reusable_(true) {}
+ reusable_(true),
+ blocked_request_buf_processed_(false) {}
HttpDownstreamConnection::~HttpDownstreamConnection() {
if (LOG_ENABLED(INFO)) {
@@ -706,6 +707,34 @@ int HttpDownstreamConnection::push_request_headers() {
return process_blocked_request_buf();
}
+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();
@@ -734,6 +763,15 @@ int HttpDownstreamConnection::process_blocked_request_buf() {
int HttpDownstreamConnection::push_upload_data_chunk(const uint8_t *data,
size_t datalen) {
+ if (should_block_request_body()) {
+ auto output = downstream_->get_blocked_request_buf();
+ auto &req = downstream_->request();
+ output->append(data, datalen);
+ req.unconsumed_body_length += datalen;
+ signal_write();
+ return 0;
+ }
+
auto chunked = downstream_->get_chunked_request();
auto output = downstream_->get_request_buf();
@@ -755,6 +793,12 @@ int HttpDownstreamConnection::push_upload_data_chunk(const uint8_t *data,
}
int HttpDownstreamConnection::end_upload_data() {
+ if (should_block_request_body()) {
+ downstream_->set_blocked_request_data_eof(true);
+ signal_write();
+ return 0;
+ }
+
signal_write();
if (!downstream_->get_chunked_request()) {
@@ -946,6 +990,11 @@ int htp_hdrs_completecb(http_parser *htp) {
// upgrade succeeded, 101 response is treated as final in nghttpx.
downstream->check_upgrade_fulfilled();
+ 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.
@@ -966,14 +1015,38 @@ int htp_hdrs_completecb(http_parser *htp) {
resp.connection_close = !http_should_keep_alive(htp);
downstream->set_response_state(Downstream::HEADER_COMPLETE);
downstream->inspect_http1_response();
+
+ if (!downstream->get_upgraded() && (htp->flags & F_CHUNKED)) {
+ downstream->set_chunked_response(true);
+ }
+
+ auto transfer_encoding = resp.fs.header(http2::HD_TRANSFER_ENCODING);
+ if (transfer_encoding && !downstream->get_chunked_response()) {
+ resp.connection_close = true;
+ }
+
if (downstream->get_upgraded()) {
// content-length must be ignored for upgraded connection.
resp.fs.content_length = -1;
resp.connection_close = true;
// transfer-encoding not applied to upgraded connection
downstream->set_chunked_response(false);
- } else if (!downstream->expect_response_body()) {
- downstream->set_chunked_response(false);
+
+ static_cast<HttpDownstreamConnection *>(dconn)
+ ->process_blocked_request_buf_on_response();
+ } else {
+ if (req.upgrade_request) {
+ resp.connection_close = true;
+ }
+
+ if (req.http_major <= 0 || (req.http_major == 1 && req.http_minor == 0)) {
+ 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()) {
@@ -1173,6 +1246,12 @@ int HttpDownstreamConnection::write_reuse_first() {
reuse_first_write_done_ = true;
+ if (should_unblock_request_body_before_response()) {
+ 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
// reset. So upstream read might still be blocked. Let's do it
diff --git a/src/shrpx_http_downstream_connection.h b/src/shrpx_http_downstream_connection.h
index 554e9b94..5bb16fe4 100644
--- a/src/shrpx_http_downstream_connection.h
+++ b/src/shrpx_http_downstream_connection.h
@@ -90,6 +90,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_;
@@ -118,6 +121,8 @@ private:
bool reuse_first_write_done_;
// true if this object can be reused
bool reusable_;
+ // 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 b2523b80..afd3c198 100644
--- a/src/shrpx_https_upstream.cc
+++ b/src/shrpx_https_upstream.cc
@@ -360,6 +360,17 @@ int htp_hdrs_completecb(http_parser *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();
@@ -484,6 +495,16 @@ int htp_bodycb(http_parser *htp, const char *data, size_t len) {
int rv;
auto upstream = static_cast<HttpsUpstream *>(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 -1;
+ }
+
rv = downstream->push_upload_data_chunk(
reinterpret_cast<const uint8_t *>(data), len);
if (rv != 0) {
@@ -508,7 +529,9 @@ int htp_msg_completecb(http_parser *htp) {
}
auto handler = upstream->get_client_handler();
auto downstream = upstream->get_downstream();
+ auto &req = downstream->request();
downstream->set_request_state(Downstream::MSG_COMPLETE);
+ req.http1_msg_complete = true;
rv = downstream->end_upload_data();
if (rv != 0) {
if (downstream->get_response_state() == Downstream::MSG_COMPLETE) {
@@ -565,7 +588,8 @@ int HttpsUpstream::on_read() {
// downstream can be nullptr here, because it is initialized in the
// callback chain called by http_parser_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());
--
2.55.0

View File

@ -1,7 +1,7 @@
Summary: Experimental HTTP/2 client, server and proxy
Name: nghttp2
Version: 1.33.0
Release: 6%{?dist}.2
Release: 6%{?dist}.3
License: MIT
Group: Applications/Internet
URL: https://nghttp2.org/
@ -22,6 +22,9 @@ Patch4: 0004-nghttp2-1.33.0-CVE-2024-28182.patch
# fix Denial of service: Assertion failure due to the missing state validation (CVE-2026-27135)
Patch5: 0005-nghttp2-1.33.0-CVE-2026-27135.patch
# fix HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests (CVE-2026-58055)
Patch6: 0006-nghttp2-1.33.0-CVE-2026-58055.patch
BuildRequires: automake
BuildRequires: libtool
@ -68,6 +71,7 @@ for building applications with libnghttp2.
%patch3 -p1
%patch4 -p1
%patch5 -p1
%patch6 -p1
autoreconf -fiv
# make fetch-ocsp-response use Python 3
@ -139,6 +143,9 @@ make %{?_smp_mflags} check
%changelog
* Wed Aug 12 2026 Jan Macku <jamacku@redhat.com> - 1.33.0-6.3
- fix HTTP Request/Response Smuggling and Response-Queue Poisoning via ambiguous HTTP/1.1 Upgrade requests (CVE-2026-58055)
* Thu Apr 09 2026 Jan Macku <jamacku@redhat.com> - 1.33.0-6.2
- fix Denial of service: Assertion failure due to the missing state validation (CVE-2026-27135)