diff --git a/SOURCES/RHEL-193803-fix-bundled-httplib2-CVE-2026-59939.patch b/SOURCES/RHEL-193803-fix-bundled-httplib2-CVE-2026-59939.patch new file mode 100644 index 0000000..adea450 --- /dev/null +++ b/SOURCES/RHEL-193803-fix-bundled-httplib2-CVE-2026-59939.patch @@ -0,0 +1,312 @@ +--- a/google/httplib2/__init__.py 2026-07-10 15:00:03.596085630 +0200 ++++ b/google/httplib2/__init__.py 2026-07-13 15:19:47.402155330 +0200 +@@ -1,6 +1,10 @@ + # -*- coding: utf-8 -*- + """Small, fast HTTP client library for Python.""" + ++import functools ++ ++from httplib2.decode import ZlibDecoder, DecoderProtocol, LimitDecoder, DeflateDecoder ++ + __author__ = "Joe Gregorio (joe@bitworking.org)" + __copyright__ = "Copyright 2006, Joe Gregorio" + __contributors__ = [ +@@ -349,23 +353,25 @@ + return retval + + +-def _decompressContent(response, new_content): ++def _decompressContent(response, new_content, limit_kwargs): + content = new_content ++ encoding_header = "content-encoding" ++ encoding = response.get(encoding_header, None) ++ limit_wrap = functools.partial(LimitDecoder, **limit_kwargs) + try: +- encoding = response.get("content-encoding", None) +- if encoding in ["gzip", "deflate"]: +- if encoding == "gzip": +- content = gzip.GzipFile(fileobj=io.BytesIO(new_content)).read() +- if encoding == "deflate": +- content = zlib.decompress(content, -zlib.MAX_WBITS) ++ if encoding in ["gzip", "deflate", "zlib"]: ++ try: ++ content = limit_wrap(ZlibDecoder()).consume_bytes(new_content, 0) ++ except (IOError, zlib.error): ++ content = limit_wrap(DeflateDecoder()).consume_bytes(new_content, 0) + response["content-length"] = str(len(content)) + # Record the historical presence of the encoding in a way the won't interfere. +- response["-content-encoding"] = response["content-encoding"] +- del response["content-encoding"] ++ response["-content-encoding"] = response.pop(encoding_header) + except (IOError, zlib.error): + content = "" + raise FailedToDecompressContent( +- _("Content purported to be compressed with %s but failed to decompress.") % response.get("content-encoding"), ++ _("Content purported to be compressed with %s but failed to decompress.") ++ % encoding, + response, + content, + ) +@@ -1212,6 +1218,10 @@ + disable_ssl_certificate_validation=False, + tls_maximum_version=None, + tls_minimum_version=None, ++ decode_limit_hard=None, ++ decode_limit_safe=None, ++ decode_limit_ratio=None, ++ decode_limit_chunk=None, + ): + """If 'cache' is a string then it is used as a directory name for + a disk cache. Otherwise it must be an object that supports the +@@ -1238,7 +1248,12 @@ + + tls_maximum_version / tls_minimum_version require Python 3.7+ / + OpenSSL 1.1.0g+. A value of "TLSv1_3" requires OpenSSL 1.1.1+. +-""" ++ ++ `decode_limit_{hard,safe,ratio,chunk}` options configure `httplib2.decode.LimitDecoder` in attempt order: ++ - Http() argument - top priority ++ - environment httplib2_decode_limit_{hard,safe,ratio,chunk} ++ - LimitDecoder defaults - least priority ++ """ + self.proxy_info = proxy_info + self.ca_certs = ca_certs + self.disable_ssl_certificate_validation = disable_ssl_certificate_validation +@@ -1286,6 +1301,22 @@ + # Keep Authorization: headers on a redirect. + self.forward_authorization_headers = False + ++ limit_kwargs = dict( ++ hard_limit=try_value_or_env( ++ int, decode_limit_hard, "httplib2_decode_limit_hard" ++ ), ++ safe_limit=try_value_or_env( ++ int, decode_limit_safe, "httplib2_decode_limit_safe" ++ ), ++ ratio=try_value_or_env( ++ float, decode_limit_ratio, "httplib2_decode_limit_ratio" ++ ), ++ chunk_size=try_value_or_env( ++ int, decode_limit_chunk, "httplib2_decode_limit_chunk" ++ ), ++ ) ++ self.limit_kwargs = {k: v for k, v in limit_kwargs.items() if v is not None} ++ + def close(self): + """Close persistent connections, clear sensitive data. + Not thread-safe, requires external synchronization against concurrent requests. +@@ -1405,7 +1436,7 @@ + content = response.read() + response = Response(response) + if method != "HEAD": +- content = _decompressContent(response, content) ++ content = _decompressContent(response, content, self.limit_kwargs) + + break + return (response, content) +@@ -1781,3 +1812,16 @@ + return self + else: + raise AttributeError(name) ++ ++ ++def try_value_or_env(to, value, env_key, default=None): ++ candidates = (value, os.environ.get(env_key), os.environ.get(env_key.upper())) ++ # same as `to(x1) or to(x2) or to(x3)` except accepting falsey values like 0 ++ for x in candidates: ++ if x is None: ++ continue ++ try: ++ return to(x) ++ except ValueError: ++ pass ++ return default +diff --git a/google/httplib2/decode.py b/google/httplib2/decode.py +new file mode 100644 +index 0000000..10540fb +--- /dev/null ++++ b/google/httplib2/decode.py +@@ -0,0 +1,183 @@ ++from typing_extensions import Protocol ++import zlib ++ ++ ++class DecodeRatioError(Exception): ++ """Output-to-input amplification ratio exceeded the configured limit.""" ++ ++ ++class DecodeLimitError(Exception): ++ """Total output length exceeded the hard limit.""" ++ ++ ++class DecoderProtocol(Protocol): ++ @property ++ def needs_input(self) -> bool: ++ ... ++ ++ def decode(self, b: bytes) -> bytes: ++ ... ++ ++ def flush(self) -> bytes: ++ ... ++ ++ def consume_bytes(self, data: bytes, chunk_size: int = 64 << 10) -> bytes: ++ out = bytearray() ++ if chunk_size == 0: ++ chunk_size = len(data) ++ for i in range(0, len(data), chunk_size): ++ chunk = data[i : i + chunk_size] ++ out.extend(self.decode(chunk)) ++ out.extend(self.flush()) ++ return bytes(out) ++ ++ ++class ZlibDecoder(DecoderProtocol): ++ """ ++ Thin wrapper around zlib.Decompressor conforming to the Decoder interface. ++ ++ Note: zlib pushes all available decompressed data immediately upon receiving ++ input. It never holds back output requiring `decode(b"")` to extract it. ++ Thus, `needs_input` naturally remains True. ++ """ ++ ++ __slots__ = ("_decoder",) ++ ++ WBITS_DEFLATE = -15 ++ WBITS_ZLIB = 15 ++ WBITS_GZIP = 15 | 16 ++ WBITS_AUTO_GZIP_ZLIB = 15 | 32 # but not deflate ++ ++ def __init__(self, wbits: int = WBITS_AUTO_GZIP_ZLIB): ++ self._decoder: zlib._Decompress | None = zlib.decompressobj(wbits) ++ ++ @property ++ def needs_input(self) -> bool: ++ if self._decoder is None: ++ raise RuntimeError("used after flush()") ++ return not self._decoder.eof ++ ++ def decode(self, b: bytes) -> bytes: ++ if self._decoder is None: ++ raise RuntimeError("used after flush()") ++ return self._decoder.decompress(b) ++ ++ def flush(self) -> bytes: ++ if self._decoder is None: ++ raise RuntimeError("used after flush()") ++ result = self._decoder.flush() ++ self._decoder = None ++ return result ++ ++ ++def DeflateDecoder() -> ZlibDecoder: ++ return ZlibDecoder(ZlibDecoder.WBITS_DEFLATE) ++ ++ ++class LimitDecoder(DecoderProtocol): ++ __slots__ = ( ++ "_decoder", ++ "_ratio", ++ "_chunk_size", ++ "_safe_limit", ++ "_hard_limit", ++ "_consumed_length", ++ "_output_length", ++ "_input_buffer", ++ "_flushed", ++ ) ++ ++ def __init__( ++ self, ++ decoder: DecoderProtocol, ++ ratio: float = 100, ++ chunk_size: int = 64 << 10, ++ safe_limit: int = 10 << 20, ++ hard_limit: int = 10 << 30, ++ ) -> None: ++ if ratio < 0: ++ raise ValueError(f"LimitDecoder() ratio={ratio} expected >= 0") ++ if chunk_size < 0: ++ raise ValueError(f"LimitDecoder() chunk_size={chunk_size} expected >= 0") ++ if safe_limit < 0: ++ raise ValueError(f"LimitDecoder() safe_limit={safe_limit} expected >= 0") ++ if hard_limit < 0: ++ raise ValueError(f"LimitDecoder() safe_limit={safe_limit} expected >= 0") ++ ++ self._decoder: DecoderProtocol = decoder ++ self._ratio: float = ratio ++ self._chunk_size: int = chunk_size ++ self._safe_limit: int = safe_limit ++ self._hard_limit: int = hard_limit ++ self._consumed_length: int = 0 ++ self._output_length: int = 0 ++ self._input_buffer: bytearray = bytearray() ++ self._flushed: bool = False ++ ++ def _check_limits(self) -> None: ++ if (self._hard_limit > 0) and (self._output_length > self._hard_limit): ++ raise DecodeLimitError(f"Output length {self._output_length} exceeds hard limit {self._hard_limit}") ++ if (self._safe_limit > 0) and (self._output_length < self._safe_limit): ++ return ++ if (self._ratio > 0) and (self._output_length > self._consumed_length * self._ratio): ++ actual_ratio = self._output_length / self._consumed_length if self._consumed_length > 0 else float("inf") ++ raise DecodeRatioError( ++ f"Amplification ratio {actual_ratio:.1f} ({self._output_length}/{self._consumed_length})" ++ f" exceeds limit {self._ratio}" ++ ) ++ ++ @property ++ def needs_input(self) -> bool: ++ return self._decoder.needs_input ++ ++ def decode(self, b: bytes) -> bytes: ++ if self._flushed: ++ raise RuntimeError("decode() called after flush()") ++ output = self._pump(b) ++ return bytes(output) ++ ++ def flush(self) -> bytes: ++ if self._flushed: ++ raise RuntimeError("flush() called more than once") ++ self._flushed = True ++ ++ output = self._pump(b"") ++ ++ data = self._decoder.flush() ++ output.extend(data) ++ self._output_length += len(data) ++ self._check_limits() ++ ++ return bytes(output) ++ ++ def _pump(self, b: bytes) -> bytearray: ++ self._input_buffer.extend(b) ++ ++ output = bytearray() ++ while True: ++ if not self._decoder.needs_input: ++ data = self._decoder.decode(b"") ++ if data: ++ output.extend(data) ++ self._output_length += len(data) ++ self._check_limits() ++ continue ++ ++ if self._input_buffer: ++ chunk = bytes(self._input_buffer[: self._chunk_size]) ++ del self._input_buffer[: self._chunk_size] ++ ++ data = self._decoder.decode(chunk) ++ self._consumed_length += len(chunk) ++ ++ if data: ++ output.extend(data) ++ self._output_length += len(data) ++ self._check_limits() ++ ++ continue ++ ++ # neither input nor decoder progress ++ break ++ ++ return output diff --git a/SPECS/fence-agents.spec b/SPECS/fence-agents.spec index fe96ea5..d90b611 100644 --- a/SPECS/fence-agents.spec +++ b/SPECS/fence-agents.spec @@ -87,7 +87,7 @@ Name: fence-agents Summary: Set of unified programs capable of host isolation ("fencing") Version: 4.2.1 -Release: 129%{?alphatag:.%{alphatag}}%{?dist}.26 +Release: 129%{?alphatag:.%{alphatag}}%{?dist}.27 License: GPLv2+ and LGPLv2+ Group: System Environment/Base URL: https://github.com/ClusterLabs/fence-agents @@ -346,6 +346,7 @@ Patch2003: RHEL-109814-2-botocore-add-SkipOsShutdown.patch Patch2004: RHEL-136027-fix-bundled-urllib3-CVE-2025-66418.patch Patch2005: RHEL-139756-fix-bundled-urllib3-CVE-2025-66471.patch Patch2006: RHEL-140783-RHEL-146288-fix-bundled-urllib3-CVE-2026-21441.patch +Patch2007: RHEL-193803-fix-bundled-httplib2-CVE-2026-59939.patch %if 0%{?fedora} || 0%{?rhel} > 7 %global supportedagents amt_ws apc apc_snmp bladecenter brocade cisco_mds cisco_ucs compute drac5 eaton_snmp emerson eps evacuate hds_cb hpblade ibmblade ibm_powervs ibm_vpc ifmib ilo ilo_moonshot ilo_mp ilo_ssh intelmodular ipdu ipmilan kdump kubevirt lpar mpath nutanix_ahv redfish rhevm rsa rsb sbd scsi vmware_rest vmware_soap wti @@ -667,11 +668,6 @@ popd pushd %{aliyunsdkvpc_dir} %{__python3} setup.py install -O1 --skip-build --root %{buildroot} --install-lib /usr/lib/fence-agents/%{bundled_lib_dir}/aliyun popd - -# google cloud -## for httplib2 install only -%{__python3} -m pip install --user --no-index --find-links %{_sourcedir} pyparsing -%{__python3} -m pip install --target %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir}/google --no-index --find-links %{_sourcedir} httplib2 %endif # aws/kubevirt @@ -694,6 +690,12 @@ rm -rf %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir}/kubevirt/rsa* # We're unable to `pip install` without cryptography and PyJWT, # so we delete them and replace them with depencies instead rm -rf %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir}/azure/{cryptography*,jwt,PyJWT-*} + +# google cloud +## for httplib2 install only +%{__python3} -m pip install --user --no-index --find-links %{_sourcedir} pyparsing +%{__python3} -m pip install --target %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir}/google --no-index --find-links %{_sourcedir} httplib2 +%{__python3} -m pip install --target %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir}/google --no-index --find-links %{_sourcedir} typing_extensions %endif # regular patch doesnt work in build-section @@ -718,6 +720,7 @@ pushd %{buildroot}/usr/lib/fence-agents/%{bundled_lib_dir} /usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2004} /usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2005} /usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2006} +/usr/bin/patch --no-backup-if-mismatch -p1 --fuzz=0 < %{PATCH2007} %endif popd @@ -1111,6 +1114,7 @@ Requires: python3-google-api-client Requires: python3-pysocks # google cloud Provides: bundled(python-httplib2) = %{httplib2_version} +Provides: bundled(python3-typing-extensions) = 4.1.1 Obsoletes: %{name} < %{version}-%{release} BuildArch: noarch %description gce @@ -1650,6 +1654,10 @@ Fence agent for IBM z/VM over IP. %endif %changelog +* Tue Jul 14 2026 Oyvind Albrigtsen - 4.2.1-129.27 +- bundled httplib2: fix CVE-2026-59939 + Resolves: RHEL-193803 + * Fri Jun 19 2026 Arslan Ahmad - 4.2.1-129.26 - fence_openstack: fix list-action to avoid timeout when there are 100+ VMs on the hypervisor