CVE-2026-42175
Description
requests-hardened is a library that overrides the default behaviors of the requests library, and adds new security features. Prior to , the SSRF protection in requests-hardened fails to block IP addresses within the RFC 6598 Shared Address Space (100.64.0.0/10). An attacker who can supply arbitrary URLs to requests-hardened could exploit this gap to access internal services hosted within 100.64.0.0/10. This is for example relevant in environments such as AWS EKS where 100.64.0.0/10 is commonly used as the default pod CIDR. The impact is environment-dependent, deployments that utilize the affected CIDR range for internal networking are exposed to SSRF bypass, while others may not be affected. This vulnerability is fixed in .
Affected products
1- Range: < 1.2.1
Patches
2a266b3958bb1fix: test failures (#61)
1 file changed · +11 −2
requests_hardened/ip_filter.py+11 −2 modified@@ -11,12 +11,21 @@ logger = logging.getLogger(__name__) # Additional CIDRs that should be blocked -EXTRA_BLOCKED_NET_RANGES: tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...] = ( +EXTRA_BLOCKED_NET_RANGES: tuple[ + Union[ipaddress.IPv4Network, ipaddress.IPv6Network], ... +] = ( ipaddress.ip_network("192.88.99.0/24"), # 6to4 relay anycast ipaddress.ip_network("100.64.0.0/10"), # CG-NAT, can route to internal workloads ipaddress.ip_network("5f00::/16"), # IPv6 Segment Routing ipaddress.ip_network("64:ff9b::/96"), # used for IPv6 & IPv4 translation (NAT64) ipaddress.ip_network("2001:20::/28"), # ORCHIDv2 (overlay identifiers) + + # Fixes https://github.com/python/cpython/issues/113171 for outdated CPython + # installations. + ipaddress.ip_network("192.0.0.0/24"), + ipaddress.ip_network("64:ff9b:1::/48"), + ipaddress.ip_network("2002::/16"), + ipaddress.ip_network("3fff::/20"), ) @@ -25,7 +34,7 @@ class InvalidIPAddress(requests.RequestException): def _is_ip_in_extra_blocked_ranges( - ip: ipaddress.IPv4Address | ipaddress.IPv6Address, + ip: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], ) -> bool: """Checks whether a given IP is part of the additional disallowed ranges."""
b7403f88d3b3Merge commit from fork
2 files changed · +100 −4
requests_hardened/ip_filter.py+25 −3 modified@@ -10,11 +10,31 @@ logger = logging.getLogger(__name__) +# Additional CIDRs that should be blocked +EXTRA_BLOCKED_NET_RANGES: tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...] = ( + ipaddress.ip_network("192.88.99.0/24"), # 6to4 relay anycast + ipaddress.ip_network("100.64.0.0/10"), # CG-NAT, can route to internal workloads + ipaddress.ip_network("5f00::/16"), # IPv6 Segment Routing + ipaddress.ip_network("64:ff9b::/96"), # used for IPv6 & IPv4 translation (NAT64) + ipaddress.ip_network("2001:20::/28"), # ORCHIDv2 (overlay identifiers) +) + class InvalidIPAddress(requests.RequestException): pass +def _is_ip_in_extra_blocked_ranges( + ip: ipaddress.IPv4Address | ipaddress.IPv6Address, +) -> bool: + """Checks whether a given IP is part of the additional disallowed ranges.""" + + for net in EXTRA_BLOCKED_NET_RANGES: + if ip in net: + return True + return False + + def get_ip_address( hostname: str, port: int, allow_loopback: bool ) -> Tuple[Union[ipaddress.IPv4Address, ipaddress.IPv6Address], int]: @@ -40,12 +60,14 @@ def get_ip_address( # private ranges, such as ::ffff:0:0/96, ::/128. # Because of that, we need to check the IPv4 address instead of the IPv6 one. # https://github.com/python/cpython/commit/ed391090cc8332406e6225d40877db6ff44a7104 - if ip.version == 6 and (ipv4 := ip.ipv4_mapped) is not None: - ip = ipv4 + if ip.version == 6: + ip = cast(ipaddress.IPv6Address, ip) + if (ipv4 := ip.ipv4_mapped) is not None: + ip = ipv4 if allow_loopback and ip.is_loopback: return ip, port - elif ip.is_private: + elif ip.is_private or ip.is_multicast or _is_ip_in_extra_blocked_ranges(ip): logger.warning( "Forbidden IP address: %s for hostname %s", ip,
tests/test_ssrf_filter.py+75 −1 modified@@ -3,7 +3,7 @@ import socket import sys from socket import AddressFamily, SocketKind -from typing import Tuple +from typing import Iterator, Tuple from unittest import mock import pytest @@ -23,6 +23,9 @@ from .utils import mock_getaddrinfo +IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address + + @pytest.mark.parametrize( "ip_addr", [ @@ -41,6 +44,13 @@ "::ffff:192.168.2.1", "::ffff:192.0.2.0", # broadcast address "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + # RFC 6598 - Shared Address Space (https://github.com/python/cpython/issues/119812) + "100.64.0.1", + "::ffff:100.64.0.1", + "::ffff:6440:1", # Compressed 100.64.0.1 + "0000:0000:0000:0000:0000:ffff:6440:0001", # Expanded 100.64.0.1 + "100.64.0.0", # first address of 100.64.0.0/10 + "100.127.255.255", # last address for 100.64.0.0/10 ], ) @pytest.mark.fake_resolver(enabled=False) @@ -55,6 +65,70 @@ def test_blocks_private_ranges(ip_addr: str): SSRFFilter.send_request("GET", "https://test.local") +@pytest.mark.parametrize( + "cidr", + [ + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "233.252.0.0/24", + "240.0.0.0/4", + "255.255.255.255/32", + "::/128", + "::1/128", + "::ffff:0:0/96", + "64:ff9b::/96", + "64:ff9b:1::/48", + "100::/64", + "2001::/32", + "2001:20::/28", + "2001:db8::/32", + "2002::/16", + "3fff::/20", + "5f00::/16", + "fc00::/7", + "fe80::/10", + "ff00::/8", + ], +) +@pytest.mark.fake_resolver(enabled=False) +def test_blocks_all_reserved_ip_address_ranges(cidr: str): + + # Tests against 3 addresses, e.g., for 127.0.0.0/8: + # - first_addr=127.0.0.0 (network address) + # - host_address=127.0.0.1 (first address) + # - last_addr=127.255.255.255 (broadcast) + net = ipaddress.ip_network(cidr) + first_addr, last_addr = net.network_address, net.broadcast_address + + # list[IPv4Address|IPv6Address] is only for Python <= 3.12. + # Should be removed once 3.12 is EOL (~Nov 2028) + hosts: Iterator[IPAddress] | list[IPAddress] = net.hosts() + host_address = hosts[0] if isinstance(hosts, list) else next(hosts) + + for addr in [first_addr, host_address, last_addr]: + assert isinstance(addr, (ipaddress.IPv4Address, ipaddress.IPv6Address)) + + with mock_getaddrinfo(str(addr)): + # (!!) We expect no connections to be made inside this test. + # If 'pytest_socket.SocketBlockedError' exception is raised, + # then something is really wrong as it means the test most likely tried + # to connect a private IP address. + with pytest.raises(InvalidIPAddress): + SSRFFilter.send_request("GET", "https://test.local") + + @pytest.mark.parametrize( "resolve_to_ip_addr, expected_sock_ip_addr", [
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-vh75-fwv3-pqrhghsaADVISORY
- github.com/saleor/requests-hardened/commit/a266b3958bb142bca515b3c230fdea19fbda327cnvd
- github.com/saleor/requests-hardened/commit/b7403f88d3b3689e57435b75b51691a160aaeef5nvd
- github.com/saleor/requests-hardened/releases/tag/v1.2.1nvd
- github.com/saleor/requests-hardened/security/advisories/GHSA-vh75-fwv3-pqrhnvd
- nvd.nist.gov/vuln/detail/CVE-2026-42175ghsa
News mentions
0No linked articles in our index yet.