From 19bfed6e0716d117730680ea7caeeb8511e9a94a Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Thu, 30 May 2024 11:54:00 +0200 Subject: [PATCH] Update to 2.32.3, fix for CVE-2024-35195 and add support for IPv6 CIDR in no_proxy setting Resolves: RHEL-32523, RHEL-37604 --- .gitignore | 1 + python-requests.spec | 23 ++- sources | 2 +- support_IPv6_CIDR_in_no_proxy.patch | 277 ++++++++++++++++++++++++++++ system-certs.patch | 61 +++--- 5 files changed, 329 insertions(+), 35 deletions(-) create mode 100644 support_IPv6_CIDR_in_no_proxy.patch diff --git a/.gitignore b/.gitignore index 67de33d..7c27bec 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ /requests-v2.28.1.tar.gz /requests-v2.28.2.tar.gz /requests-v2.31.0.tar.gz +/requests-v2.32.3.tar.gz diff --git a/python-requests.spec b/python-requests.spec index 35b999c..6cdb78f 100644 --- a/python-requests.spec +++ b/python-requests.spec @@ -5,8 +5,8 @@ %bcond extras %{undefined rhel} Name: python-requests -Version: 2.31.0 -Release: 4%{?dist} +Version: 2.32.3 +Release: 1%{?dist} Summary: HTTP library, written in Python, for human beings License: Apache-2.0 @@ -17,6 +17,13 @@ Source: https://github.com/requests/requests/archive/v%{version}/request # https://bugzilla.redhat.com/show_bug.cgi?id=904614 Patch: system-certs.patch +# Add support for IPv6 CIDR in no_proxy setting +# This functionality is needed in Openshift and it has been +# proposed for upstream in 2021 but the PR unfortunately stalled. +# Upstream PR: https://github.com/psf/requests/pull/5953 +# This change is backported also into RHEL 9.4 (via CS) +Patch: support_IPv6_CIDR_in_no_proxy.patch + BuildArch: noarch BuildRequires: python%{python3_pkgversion}-devel %if %{with tests} @@ -56,7 +63,7 @@ designed to make HTTP requests easy for developers. %autosetup -p1 -n requests-%{version} # env shebang in nonexecutable file -sed -i '/#!\/usr\/.*python/d' requests/certs.py +sed -i '/#!\/usr\/.*python/d' src/requests/certs.py # Some doctests use the internet and fail to pass in Koji. Since doctests don't have names, I don't # know a way to skip them. We also don't want to patch them out, because patching them out will @@ -76,8 +83,9 @@ sed -i 's/ --doctest-modules//' pyproject.toml %check %pyproject_check_import %if %{with tests} +# test_unicode_header_name - reported: https://github.com/psf/requests/issues/6734 # test_use_proxy_from_environment needs pysocks -%pytest -v tests %{!?with_extras:-k "not test_use_proxy_from_environment"} +%pytest -v tests -k "not test_unicode_header_name %{!?with_extras:and not test_use_proxy_from_environment}" %endif @@ -87,6 +95,13 @@ sed -i 's/ --doctest-modules//' pyproject.toml %changelog +* Wed Jun 26 2024 Lumír Balhar - 2.32.3-1 +- Update to 2.32.3 +- Fix for CVE-2024-35195 +Resolves: RHEL-37604 +- Add support for IPv6 CIDR in no_proxy setting +Resolves: RHEL-32523 + * Mon Jun 24 2024 Troy Dawson - 2.31.0-4 - Bump release for June 2024 mass rebuild diff --git a/sources b/sources index 3e4fef8..2c91835 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (requests-v2.31.0.tar.gz) = 43f536bdb2360fcceb24ef98e995ffa66cdefc2c502629f17a5722445bfa9ad8489201958c846c2aaef37e427f95a4d56e321a91095c69754680abfd83b39150 +SHA512 (requests-v2.32.3.tar.gz) = ea3e85e035efed0fe22bf8640491ffb20c2ac50359bb1e11c9147ed850cac5e4a6a36ab58a48fc6c6d6a44df2b511e8a5d902444c034da6baa6adc5f9417697f diff --git a/support_IPv6_CIDR_in_no_proxy.patch b/support_IPv6_CIDR_in_no_proxy.patch new file mode 100644 index 0000000..93bcff9 --- /dev/null +++ b/support_IPv6_CIDR_in_no_proxy.patch @@ -0,0 +1,277 @@ +From 91526670ad66e83e799459cb23b031b88bb680b4 Mon Sep 17 00:00:00 2001 +From: Derek Higgins +Date: Thu, 30 May 2024 11:15:18 +0200 +Subject: [PATCH 2/2] Add ipv6 support to should_bypass_proxies + +Add support to should_bypass_proxies to support +IPv6 ipaddresses and CIDRs in no_proxy. Includes +adding IPv6 support to various other helper functions. + +Co-authored-by: Lumir Balhar +--- + src/requests/utils.py | 83 ++++++++++++++++++++++++++++++++++++------- + tests/test_utils.py | 66 +++++++++++++++++++++++++++++++--- + 2 files changed, 132 insertions(+), 17 deletions(-) + +diff --git a/src/requests/utils.py b/src/requests/utils.py +index ae6c42f..0363698 100644 +--- a/src/requests/utils.py ++++ b/src/requests/utils.py +@@ -679,18 +679,46 @@ def requote_uri(uri): + return quote(uri, safe=safe_without_percent) + + ++def _get_mask_bits(mask, totalbits=32): ++ """Converts a mask from /xx format to a int ++ to be used as a mask for IP's in int format ++ ++ Example: if mask is 24 function returns 0xFFFFFF00 ++ if mask is 24 and totalbits=128 function ++ returns 0xFFFFFF00000000000000000000000000 ++ ++ :rtype: int ++ """ ++ bits = ((1 << mask) - 1) << (totalbits - mask) ++ return bits ++ ++ + def address_in_network(ip, net): + """This function allows you to check if an IP belongs to a network subnet + + Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24 + returns False if ip = 192.168.1.1 and net = 192.168.100.0/24 ++ returns True if ip = 1:2:3:4::1 and net = 1:2:3:4::/64 + + :rtype: bool + """ +- ipaddr = struct.unpack("=L", socket.inet_aton(ip))[0] + netaddr, bits = net.split("/") +- netmask = struct.unpack("=L", socket.inet_aton(dotted_netmask(int(bits))))[0] +- network = struct.unpack("=L", socket.inet_aton(netaddr))[0] & netmask ++ if is_ipv4_address(ip) and is_ipv4_address(netaddr): ++ ipaddr = struct.unpack(">L", socket.inet_aton(ip))[0] ++ netmask = _get_mask_bits(int(bits)) ++ network = struct.unpack(">L", socket.inet_aton(netaddr))[0] ++ elif is_ipv6_address(ip) and is_ipv6_address(netaddr): ++ ipaddr_msb, ipaddr_lsb = struct.unpack( ++ ">QQ", socket.inet_pton(socket.AF_INET6, ip) ++ ) ++ ipaddr = (ipaddr_msb << 64) ^ ipaddr_lsb ++ netmask = _get_mask_bits(int(bits), 128) ++ network_msb, network_lsb = struct.unpack( ++ ">QQ", socket.inet_pton(socket.AF_INET6, netaddr) ++ ) ++ network = (network_msb << 64) ^ network_lsb ++ else: ++ return False + return (ipaddr & netmask) == (network & netmask) + + +@@ -710,12 +738,39 @@ def is_ipv4_address(string_ip): + :rtype: bool + """ + try: +- socket.inet_aton(string_ip) ++ socket.inet_pton(socket.AF_INET, string_ip) ++ except OSError: ++ return False ++ return True ++ ++ ++def is_ipv6_address(string_ip): ++ """ ++ :rtype: bool ++ """ ++ try: ++ socket.inet_pton(socket.AF_INET6, string_ip) + except OSError: + return False + return True + + ++def compare_ips(a, b): ++ """ ++ Compare 2 IP's, uses socket.inet_pton to normalize IPv6 IPs ++ ++ :rtype: bool ++ """ ++ if a == b: ++ return True ++ try: ++ return socket.inet_pton(socket.AF_INET6, a) == socket.inet_pton( ++ socket.AF_INET6, b ++ ) ++ except OSError: ++ return False ++ ++ + def is_valid_cidr(string_network): + """ + Very simple check of the cidr format in no_proxy variable. +@@ -723,17 +778,19 @@ def is_valid_cidr(string_network): + :rtype: bool + """ + if string_network.count("/") == 1: ++ address, mask = string_network.split("/") + try: +- mask = int(string_network.split("/")[1]) ++ mask = int(mask) + except ValueError: + return False + +- if mask < 1 or mask > 32: +- return False +- +- try: +- socket.inet_aton(string_network.split("/")[0]) +- except OSError: ++ if is_ipv4_address(address): ++ if mask < 1 or mask > 32: ++ return False ++ elif is_ipv6_address(address): ++ if mask < 1 or mask > 128: ++ return False ++ else: + return False + else: + return False +@@ -790,12 +847,12 @@ def should_bypass_proxies(url, no_proxy): + # the end of the hostname, both with and without the port. + no_proxy = (host for host in no_proxy.replace(" ", "").split(",") if host) + +- if is_ipv4_address(parsed.hostname): ++ if is_ipv4_address(parsed.hostname) or is_ipv6_address(parsed.hostname): + for proxy_ip in no_proxy: + if is_valid_cidr(proxy_ip): + if address_in_network(parsed.hostname, proxy_ip): + return True +- elif parsed.hostname == proxy_ip: ++ elif compare_ips(parsed.hostname, proxy_ip): + # If no_proxy ip was defined in plain IP notation instead of cidr notation & + # matches the IP of the index + return True +diff --git a/tests/test_utils.py b/tests/test_utils.py +index 5e9b56e..befbb46 100644 +--- a/tests/test_utils.py ++++ b/tests/test_utils.py +@@ -14,9 +14,11 @@ from requests._internal_utils import unicode_is_ascii + from requests.cookies import RequestsCookieJar + from requests.structures import CaseInsensitiveDict + from requests.utils import ( ++ _get_mask_bits, + _parse_content_type_header, + add_dict_to_cookiejar, + address_in_network, ++ compare_ips, + dotted_netmask, + extract_zipped_paths, + get_auth_from_url, +@@ -263,8 +265,15 @@ class TestIsIPv4Address: + + + class TestIsValidCIDR: +- def test_valid(self): +- assert is_valid_cidr("192.168.1.0/24") ++ @pytest.mark.parametrize( ++ "value", ++ ( ++ "192.168.1.0/24", ++ "1:2:3:4::/64", ++ ), ++ ) ++ def test_valid(self, value): ++ assert is_valid_cidr(value) + + @pytest.mark.parametrize( + "value", +@@ -274,6 +283,11 @@ class TestIsValidCIDR: + "192.168.1.0/128", + "192.168.1.0/-1", + "192.168.1.999/24", ++ "1:2:3:4::1", ++ "1:2:3:4::/a", ++ "1:2:3:4::0/321", ++ "1:2:3:4::/-1", ++ "1:2:3:4::12211/64", + ), + ) + def test_invalid(self, value): +@@ -287,6 +301,12 @@ class TestAddressInNetwork: + def test_invalid(self): + assert not address_in_network("172.16.0.1", "192.168.1.0/24") + ++ def test_valid_v6(self): ++ assert address_in_network("1:2:3:4::1111", "1:2:3:4::/64") ++ ++ def test_invalid_v6(self): ++ assert not address_in_network("1:2:3:4:1111", "1:2:3:4::/124") ++ + + class TestGuessFilename: + @pytest.mark.parametrize( +@@ -722,6 +742,11 @@ def test_urldefragauth(url, expected): + ("http://172.16.1.12:5000/", False), + ("http://google.com:5000/v1.0/", False), + ("file:///some/path/on/disk", True), ++ ("http://[1:2:3:4:5:6:7:8]:5000/", True), ++ ("http://[1:2:3:4::1]/", True), ++ ("http://[1:2:3:9::1]/", True), ++ ("http://[1:2:3:9:0:0:0:1]/", True), ++ ("http://[1:2:3:9::2]/", False), + ), + ) + def test_should_bypass_proxies(url, expected, monkeypatch): +@@ -730,11 +755,11 @@ def test_should_bypass_proxies(url, expected, monkeypatch): + """ + monkeypatch.setenv( + "no_proxy", +- "192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1, google.com:6000", ++ "192.168.0.0/24,127.0.0.1,localhost.localdomain,1:2:3:4::/64,1:2:3:9::1,172.16.1.1, google.com:6000", + ) + monkeypatch.setenv( + "NO_PROXY", +- "192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1, google.com:6000", ++ "192.168.0.0/24,127.0.0.1,localhost.localdomain,1:2:3:4::/64,1:2:3:9::1,172.16.1.1, google.com:6000", + ) + assert should_bypass_proxies(url, no_proxy=None) == expected + +@@ -956,3 +981,36 @@ def test_should_bypass_proxies_win_registry_ProxyOverride_value(monkeypatch): + monkeypatch.setattr(winreg, "OpenKey", OpenKey) + monkeypatch.setattr(winreg, "QueryValueEx", QueryValueEx) + assert should_bypass_proxies("http://example.com/", None) is False ++ ++ ++@pytest.mark.parametrize( ++ "mask, totalbits, maskbits", ++ ( ++ (24, None, 0xFFFFFF00), ++ (31, None, 0xFFFFFFFE), ++ (0, None, 0x0), ++ (4, 4, 0xF), ++ (24, 128, 0xFFFFFF00000000000000000000000000), ++ ), ++) ++def test__get_mask_bits(mask, totalbits, maskbits): ++ args = {"mask": mask} ++ if totalbits: ++ args["totalbits"] = totalbits ++ assert _get_mask_bits(**args) == maskbits ++ ++ ++@pytest.mark.parametrize( ++ "a, b, expected", ++ ( ++ ("1.2.3.4", "1.2.3.4", True), ++ ("1.2.3.4", "2.2.3.4", False), ++ ("1::4", "1.2.3.4", False), ++ ("1::4", "1::4", True), ++ ("1::4", "1:0:0:0:0:0:0:4", True), ++ ("1::4", "1:0:0:0:0:0::4", True), ++ ("1::4", "1:0:0:0:0:0:1:4", False), ++ ), ++) ++def test_compare_ips(a, b, expected): ++ assert compare_ips(a, b) == expected +-- +2.45.1 + diff --git a/system-certs.patch b/system-certs.patch index 0e323f8..efc0010 100644 --- a/system-certs.patch +++ b/system-certs.patch @@ -1,36 +1,17 @@ -From fbe9d1e38da72d8caac365841794b06614523ac9 Mon Sep 17 00:00:00 2001 +From bb733473e91e71b812ada46bc110f607630f9327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hrn=C4=8Diar?= -Date: Tue, 17 Oct 2023 10:44:42 +0200 -Subject: [PATCH] system certs +Date: Thu, 30 May 2024 11:10:29 +0200 +Subject: [PATCH 1/2] system certs Co-authored-by: Lumir Balhar --- - requests/certs.py | 7 ++++++- - setup.cfg | 1 - - setup.py | 1 - - 3 files changed, 6 insertions(+), 3 deletions(-) + setup.cfg | 1 - + setup.py | 1 - + src/requests/certs.py | 8 +++++++- + 3 files changed, 7 insertions(+), 3 deletions(-) -diff --git a/requests/certs.py b/requests/certs.py -index 1f30a45..9224f62 100644 ---- a/requests/certs.py -+++ b/requests/certs.py -@@ -9,8 +9,13 @@ only one — the one from the certifi package. - If you are packaging Requests, e.g., for a Linux distribution or a managed - environment, you can change the definition of where() to return a separately - packaged CA bundle. -+ -+This Fedora-patched package returns "/etc/pki/tls/certs/ca-bundle.crt" provided -+by the ca-certificates RPM package. - """ --from certifi import where -+def where(): -+ """Return the absolute path to the system CA bundle.""" -+ return '/etc/pki/tls/certs/ca-bundle.crt' - - if __name__ == "__main__": - print(where()) diff --git a/setup.cfg b/setup.cfg -index bf21c81..906c0f1 100644 +index 8d44e0e..fa10a53 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,6 @@ provides-extra = @@ -40,9 +21,9 @@ index bf21c81..906c0f1 100644 - certifi>=2017.4.17 charset_normalizer>=2,<4 idna>=2.5,<4 - urllib3>=1.21.1,<1.27 + urllib3>=1.21.1,<3 diff --git a/setup.py b/setup.py -index 0123545..ec3fd1d 100755 +index 1b0eb37..03d19b0 100755 --- a/setup.py +++ b/setup.py @@ -62,7 +62,6 @@ requires = [ @@ -53,6 +34,26 @@ index 0123545..ec3fd1d 100755 ] test_requirements = [ "pytest-httpbin==2.0.0", +diff --git a/src/requests/certs.py b/src/requests/certs.py +index be422c3..9aee713 100644 +--- a/src/requests/certs.py ++++ b/src/requests/certs.py +@@ -10,8 +10,14 @@ only one — the one from the certifi package. + If you are packaging Requests, e.g., for a Linux distribution or a managed + environment, you can change the definition of where() to return a separately + packaged CA bundle. ++ ++This Fedora-patched package returns "/etc/pki/tls/certs/ca-bundle.crt" provided ++by the ca-certificates RPM package. + """ +-from certifi import where ++ ++def where(): ++ """Return the absolute path to the system CA bundle.""" ++ return '/etc/pki/tls/certs/ca-bundle.crt' + + if __name__ == "__main__": + print(where()) -- -2.41.0 +2.45.1