Security fix for CVE-2026-44431 and CVE-2026-44432
- Resolves: RHEL-184817 - Resolves: RHEL-185121
This commit is contained in:
parent
d89bffa457
commit
b520843218
148
CVE-2026-44431.patch
Normal file
148
CVE-2026-44431.patch
Normal file
@ -0,0 +1,148 @@
|
||||
From 7d77cb649bbbb46711d39da4075a2b0f95894fcd Mon Sep 17 00:00:00 2001
|
||||
From: Illia Volochii <illia.volochii@gmail.com>
|
||||
Date: Wed, 3 Jun 2026 12:48:11 +0200
|
||||
Subject: [PATCH] CVE-2026-44431
|
||||
|
||||
* Remove sensitive headers in proxy pools too
|
||||
|
||||
* Add a changelog entry
|
||||
|
||||
* Check retries history in tests
|
||||
|
||||
Co-authored-by: Copilot <copilot@github.com>
|
||||
|
||||
---------
|
||||
|
||||
Co-authored-by: Copilot <copilot@github.com>
|
||||
---
|
||||
changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst | 3 +
|
||||
src/urllib3/connectionpool.py | 12 ++++
|
||||
.../test_proxy_poolmanager.py | 72 +++++++++++++++++++
|
||||
3 files changed, 87 insertions(+)
|
||||
create mode 100644 changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst
|
||||
|
||||
diff --git a/changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst b/changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst
|
||||
new file mode 100644
|
||||
index 0000000..bac765e
|
||||
--- /dev/null
|
||||
+++ b/changelog/GHSA-qccp-gfcp-xxvc.bugfix.rst
|
||||
@@ -0,0 +1,3 @@
|
||||
+Fixed HTTP pools created using ``ProxyManager.connection_from_url`` to strip
|
||||
+sensitive headers specified in ``Retry.remove_headers_on_redirect`` when
|
||||
+redirecting to a different host.
|
||||
diff --git a/src/urllib3/connectionpool.py b/src/urllib3/connectionpool.py
|
||||
index 402bf67..9466b32 100644
|
||||
--- a/src/urllib3/connectionpool.py
|
||||
+++ b/src/urllib3/connectionpool.py
|
||||
@@ -852,6 +852,18 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
|
||||
body = None
|
||||
headers = HTTPHeaderDict(headers)._prepare_for_method_change()
|
||||
|
||||
+ # Strip headers marked as unsafe to forward to the redirected location.
|
||||
+ # Check remove_headers_on_redirect to avoid a potential network call within
|
||||
+ # self.is_same_host() which may use socket.gethostbyname() in the future.
|
||||
+ if retries.remove_headers_on_redirect and not self.is_same_host(
|
||||
+ redirect_location
|
||||
+ ):
|
||||
+ new_headers = headers.copy() # type: ignore[union-attr]
|
||||
+ for header in headers:
|
||||
+ if header.lower() in retries.remove_headers_on_redirect:
|
||||
+ new_headers.pop(header, None)
|
||||
+ headers = new_headers
|
||||
+
|
||||
try:
|
||||
retries = retries.increment(method, url, response=response, _pool=self)
|
||||
except MaxRetryError:
|
||||
diff --git a/test/with_dummyserver/test_proxy_poolmanager.py b/test/with_dummyserver/test_proxy_poolmanager.py
|
||||
index 7b292a2..b8745fc 100644
|
||||
--- a/test/with_dummyserver/test_proxy_poolmanager.py
|
||||
+++ b/test/with_dummyserver/test_proxy_poolmanager.py
|
||||
@@ -34,6 +34,7 @@ from urllib3.exceptions import (
|
||||
SubjectAltNameWarning,
|
||||
)
|
||||
from urllib3.poolmanager import ProxyManager, proxy_from_url
|
||||
+from urllib3.util.retry import RequestHistory
|
||||
from urllib3.util import Timeout
|
||||
from urllib3.util.ssl_ import create_urllib3_context
|
||||
|
||||
@@ -277,6 +278,77 @@ class TestHTTPProxyManager(HTTPDummyProxyTestCase):
|
||||
)
|
||||
assert r._pool.host != self.http_host_alt
|
||||
|
||||
+ _sensitive_headers = {
|
||||
+ "Authorization": "foo",
|
||||
+ "Proxy-Authorization": "bar",
|
||||
+ "Cookie": "foo=bar",
|
||||
+ }
|
||||
+
|
||||
+ @pytest.mark.parametrize(
|
||||
+ "sensitive_headers",
|
||||
+ (_sensitive_headers, {k.lower(): v for k, v in _sensitive_headers.items()}),
|
||||
+ ids=("capitalized", "lowercase"),
|
||||
+ )
|
||||
+ def test_cross_host_redirect_remove_headers_via_proxy_manager(
|
||||
+ self, sensitive_headers: dict[str, str]
|
||||
+ ) -> None:
|
||||
+ headers_url = f"{self.http_url_alt}/headers"
|
||||
+ initial_url = f"{self.http_url}/redirect?target={headers_url}"
|
||||
+ with proxy_from_url(self.proxy_url) as proxy_mgr:
|
||||
+ r = proxy_mgr.request(
|
||||
+ "GET", initial_url, headers=sensitive_headers, retries=1
|
||||
+ )
|
||||
+ assert r.status == 200
|
||||
+ assert r.retries is not None
|
||||
+ assert r.retries.history == (
|
||||
+ RequestHistory(
|
||||
+ method="GET",
|
||||
+ url=initial_url,
|
||||
+ error=None,
|
||||
+ status=303,
|
||||
+ redirect_location=headers_url,
|
||||
+ ),
|
||||
+ )
|
||||
+ data = r.json()
|
||||
+ for header in sensitive_headers:
|
||||
+ assert header not in data
|
||||
+
|
||||
+ @pytest.mark.parametrize(
|
||||
+ "sensitive_headers",
|
||||
+ (_sensitive_headers, {k.lower(): v for k, v in _sensitive_headers.items()}),
|
||||
+ ids=("capitalized", "lowercase"),
|
||||
+ )
|
||||
+ def test_cross_host_redirect_remove_headers_via_pool(
|
||||
+ self, sensitive_headers: dict[str, str]
|
||||
+ ) -> None:
|
||||
+ headers_url = f"{self.http_url_alt}/headers"
|
||||
+ initial_url = f"{self.http_url}/redirect?target={headers_url}"
|
||||
+ with proxy_from_url(self.proxy_url) as proxy_mgr:
|
||||
+ pool = proxy_mgr.connection_from_url(self.http_url)
|
||||
+ r = pool.urlopen(
|
||||
+ "GET",
|
||||
+ initial_url,
|
||||
+ headers=sensitive_headers,
|
||||
+ retries=1,
|
||||
+ redirect=True,
|
||||
+ assert_same_host=False,
|
||||
+ preload_content=True,
|
||||
+ )
|
||||
+ assert r.status == 200
|
||||
+ assert r.retries is not None
|
||||
+ assert r.retries.history == (
|
||||
+ RequestHistory(
|
||||
+ method="GET",
|
||||
+ url=initial_url,
|
||||
+ error=None,
|
||||
+ status=303,
|
||||
+ redirect_location=headers_url,
|
||||
+ ),
|
||||
+ )
|
||||
+ data = r.json()
|
||||
+ for header in sensitive_headers:
|
||||
+ assert header not in data
|
||||
+
|
||||
def test_cross_protocol_redirect(self):
|
||||
with proxy_from_url(self.proxy_url, ca_certs=DEFAULT_CA) as http:
|
||||
cross_protocol_location = "%s/echo?a=b" % self.https_url
|
||||
--
|
||||
2.54.0
|
||||
|
||||
137
CVE-2026-44432.patch
Normal file
137
CVE-2026-44432.patch
Normal file
@ -0,0 +1,137 @@
|
||||
From 377ecb4c3bb1bc5e196f13ed7418c3d4b6380afc Mon Sep 17 00:00:00 2001
|
||||
From: Illia Volochii <illia.volochii@gmail.com>
|
||||
Date: Wed, 3 Jun 2026 13:02:33 +0200
|
||||
Subject: [PATCH] CVE-2026-44432
|
||||
|
||||
* Avoid any decoding in `HTTPResponse.drain_conn`
|
||||
|
||||
* Add a comment
|
||||
|
||||
* Simplify `drain_conn`
|
||||
|
||||
* Add tests
|
||||
|
||||
* Add additional checks to the test
|
||||
|
||||
* Fix full decompression on the 2nd small read from response using Brotli
|
||||
|
||||
* Add a changelog entry
|
||||
|
||||
* Inverse the order in the changelog entry
|
||||
|
||||
* Mention `stream` call
|
||||
---
|
||||
changelog/GHSA-mf9v-mfxr-j63j.bugfix.rst | 7 +++++++
|
||||
src/urllib3/response.py | 17 +++++++++++------
|
||||
test/test_response.py | 24 +++++++++++++++++++++---
|
||||
3 files changed, 39 insertions(+), 9 deletions(-)
|
||||
create mode 100644 changelog/GHSA-mf9v-mfxr-j63j.bugfix.rst
|
||||
|
||||
diff --git a/changelog/GHSA-mf9v-mfxr-j63j.bugfix.rst b/changelog/GHSA-mf9v-mfxr-j63j.bugfix.rst
|
||||
new file mode 100644
|
||||
index 0000000..ac70af8
|
||||
--- /dev/null
|
||||
+++ b/changelog/GHSA-mf9v-mfxr-j63j.bugfix.rst
|
||||
@@ -0,0 +1,7 @@
|
||||
+Fixed two high-severity security issues where decompression-bomb safeguards of the streaming API were bypassed:
|
||||
+
|
||||
+
|
||||
+1. When ``HTTPResponse.drain_conn()`` was called after the response had been read and decompressed partially.
|
||||
+2. During the second ``HTTPResponse.read(amt=N)`` or ``HTTPResponse.stream(amt=N)`` call when the response was decompressed using the official `Brotli <https://pypi.org/project/brotli/>`__ library.
|
||||
+
|
||||
+See `GHSA-mf9v-mfxr-j63j <https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j>`__ for details.
|
||||
diff --git a/src/urllib3/response.py b/src/urllib3/response.py
|
||||
index 1357d65..76c56d2 100644
|
||||
--- a/src/urllib3/response.py
|
||||
+++ b/src/urllib3/response.py
|
||||
@@ -480,13 +480,14 @@ class HTTPResponse(io.IOBase):
|
||||
Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.
|
||||
"""
|
||||
try:
|
||||
- self.read(
|
||||
- # Do not spend resources decoding the content unless
|
||||
- # decoding has already been initiated.
|
||||
- decode_content=self._has_decoded_content,
|
||||
- )
|
||||
+ self._raw_read()
|
||||
except (HTTPError, SocketError, BaseSSLError, HTTPException):
|
||||
pass
|
||||
+ if self._has_decoded_content:
|
||||
+ # `_raw_read` skips decompression, so we should clean up the
|
||||
+ # decoder to avoid keeping unnecessary data in memory.
|
||||
+ self._decoded_buffer = BytesQueueBuffer()
|
||||
+ self._decoder = None
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
@@ -800,7 +801,11 @@ class HTTPResponse(io.IOBase):
|
||||
if amt is not None:
|
||||
cache_content = False
|
||||
|
||||
- if self._decoder and self._decoder.has_unconsumed_tail:
|
||||
+ if (
|
||||
+ self._decoder
|
||||
+ and self._decoder.has_unconsumed_tail
|
||||
+ and len(self._decoded_buffer) < amt
|
||||
+ ):
|
||||
decoded_data = self._decode(
|
||||
b"",
|
||||
decode_content,
|
||||
diff --git a/test/test_response.py b/test/test_response.py
|
||||
index 597ce46..b0539e5 100644
|
||||
--- a/test/test_response.py
|
||||
+++ b/test/test_response.py
|
||||
@@ -469,22 +469,33 @@ class TestResponse(object):
|
||||
pytest.skip(f"Proper {request.node.callspec.id} decoder is not available")
|
||||
|
||||
name, compressed_data = data
|
||||
- limit = 1024 * 1024 # 1 MiB
|
||||
+ limit1 = 1024 * 1024 # 1 MiB
|
||||
+ # We test with two read calls because the second call may be
|
||||
+ # able to use the internal buffer filled by the first call, and
|
||||
+ # we want to ensure that full decompression is never triggered
|
||||
+ # by the second call. The limit for the second call is lowered
|
||||
+ # to make sure that the internal buffer is used for the Brotli
|
||||
+ # case specifically https://github.com/google/brotli/issues/1396
|
||||
+ limit2 = 1024 # 1 KiB
|
||||
if read_method in ("read_chunked", "stream"):
|
||||
httplib_r = httplib.HTTPResponse(MockSock) # type: ignore[arg-type]
|
||||
+ httplib_r.chunked = True
|
||||
+ httplib_r.chunk_left = 1
|
||||
httplib_r.fp = MockChunkedEncodingResponse([compressed_data]) # type: ignore[assignment]
|
||||
r = HTTPResponse(
|
||||
httplib_r,
|
||||
preload_content=False,
|
||||
headers={"transfer-encoding": "chunked", "content-encoding": name},
|
||||
)
|
||||
- next(getattr(r, read_method)(amt=limit, decode_content=True))
|
||||
+ for limit in (limit1, limit2):
|
||||
+ next(getattr(r, read_method)(amt=limit, decode_content=True))
|
||||
else:
|
||||
fp = BytesIO(compressed_data)
|
||||
r = HTTPResponse(
|
||||
fp, headers={"content-encoding": name}, preload_content=False
|
||||
)
|
||||
- getattr(r, read_method)(amt=limit, decode_content=True)
|
||||
+ for limit in (limit1, limit2):
|
||||
+ getattr(r, read_method)(amt=limit, decode_content=True)
|
||||
|
||||
# Check that the internal decoded buffer is empty unless brotli
|
||||
# is used.
|
||||
@@ -494,6 +505,13 @@ class TestResponse(object):
|
||||
if name != "br" or brotli.__name__ == "brotlicffi":
|
||||
assert len(r._decoded_buffer) == 0
|
||||
|
||||
+ # Check that memory usage is still within the limit while the
|
||||
+ # connection is being drained, meaning that the call does not
|
||||
+ # decompress the whole content.
|
||||
+ r.drain_conn()
|
||||
+ assert r._decoder is None
|
||||
+ assert len(r._decoded_buffer) == 0
|
||||
+
|
||||
def test_multi_decoding_deflate_deflate(self):
|
||||
data = zlib.compress(zlib.compress(b"foo"))
|
||||
|
||||
--
|
||||
2.54.0
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
Name: python-urllib3
|
||||
Version: 1.26.19
|
||||
Release: 3%{?dist}
|
||||
Release: 4%{?dist}
|
||||
Summary: HTTP library with thread-safe connection pooling, file post, and more
|
||||
|
||||
# SPDX
|
||||
@ -20,6 +20,8 @@ Source: %{url}/archive/%{version}/urllib3-%{version}.tar.gz
|
||||
Patch: CVE-2025-66471.patch
|
||||
Patch: CVE-2025-66418.patch
|
||||
Patch: CVE-2026-21441.patch
|
||||
Patch: CVE-2026-44431.patch
|
||||
Patch: CVE-2026-44432.patch
|
||||
|
||||
BuildArch: noarch
|
||||
|
||||
@ -171,6 +173,11 @@ ignore="${ignore-} --ignore=test/test_no_ssl.py"
|
||||
|
||||
|
||||
%changelog
|
||||
* Wed Jun 03 2026 Tomáš Hrnčiar <thrnciar@redhat.com> - 1.26.19-4
|
||||
- Security fix for CVE-2026-44431
|
||||
- Security fix for CVE-2026-44432
|
||||
Resolves: RHEL-184817, RHEL-185121
|
||||
|
||||
* Tue Jan 27 2026 Miro Hrončok <mhroncok@redhat.com> - 1.26.19-3
|
||||
- Security fix for CVE-2025-66471
|
||||
- Security fix for CVE-2025-66418
|
||||
|
||||
Loading…
Reference in New Issue
Block a user