From 885a5e830bf22396607769ee36d9bc3c4e35126f Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Thu, 5 Nov 2026 08:25:45 -0600 Subject: [PATCH 1/3] Respect domain boundaries when matching no_proxy entries should_bypass_proxies matched a no_proxy entry with a plain hostname.endswith(entry), so no_proxy=example.com also bypassed the proxy for badexample.com, and that traffic went direct instead of through the configured proxy (which may be the egress filter or the only allowed route). An entry now matches only the exact host (optionally host:port) or a subdomain of it; a leading dot on the entry is ignored. This is the requests port of CPython bpo-39057. Python 2.7's own urllib.proxy_bypass_environment, which requests falls back to, already matches this way. Upstream: psf/requests a4f9a599 (#7427), released in 2.34.0. No CVE or GHSA. Test adapted from upstream, plus a badanotherdomain.com case. Python 2.7 safe. Co-Authored-By: Claude --- requests/utils.py | 7 +++++++ tests/test_utils.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/requests/utils.py b/requests/utils.py index 0ac98e6ace..ed5f062e1c 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -778,6 +778,13 @@ def should_bypass_proxies(url, no_proxy): host_with_port += ':{}'.format(parsed.port) for host in no_proxy: + # Match whole domain labels only, so that "example.com" does + # not also bypass the proxy for "badexample.com". A leading + # dot on the entry is ignored. (CPython bpo-39057) + host = host.lstrip('.') + if parsed.hostname == host or host_with_port == host: + return True + host = '.' + host if parsed.hostname.endswith(host) or host_with_port.endswith(host): # The URL does match something in no_proxy, so we don't want # to apply the proxies on this URL. diff --git a/tests/test_utils.py b/tests/test_utils.py index 8e98397204..26b959a5a0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -728,6 +728,28 @@ def test_should_bypass_proxies_no_proxy( assert should_bypass_proxies(url, no_proxy=no_proxy) == expected +@pytest.mark.parametrize( + 'url, expected', ( + ('http://localhost/', True), + ('http://anotherdomain.com:8888/', True), + ('http://newdomain.com:1234/', True), + ('http://www.newdomain.com:1234/', True), + ('http://foo.d.o.t/', True), + ('http://d.o.t/', True), + ('http://prelocalhost/', False), + ('http://badanotherdomain.com/', False), + ('http://newdomain.com/', False), + ('http://newdomain.com:1235/', False), + )) +def test_should_bypass_proxies_no_proxy_domain_boundary(url, expected): + """Ensure no_proxy matching respects domain boundaries and does not + greedily match domains that merely end with the no_proxy entry. + See CPython bpo-39057 and requests #7427. + """ + no_proxy = 'localhost, anotherdomain.com, newdomain.com:1234, .d.o.t' + assert should_bypass_proxies(url, no_proxy=no_proxy) == expected + + @pytest.mark.skipif(os.name != 'nt', reason='Test only on Windows') @pytest.mark.parametrize( 'url, expected, override', ( From c9b2b69fc1ce502a31d20231e1242d85f5103f38 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Thu, 5 Nov 2026 08:26:39 -0600 Subject: [PATCH 2/3] Accept post-release urllib3 versions in check_compatibility ActiveState's urllib3 security builds now use PEP 440 post-releases (1.26.20.post3, 1.26.20.post4) instead of +security.N local versions. check_compatibility split "1.26.20.post4" into four parts, so the major/minor/patch unpack raised ValueError and every "import requests" emitted a RequestsDependencyWarning (an ImportError under -W error). Only the first three components are parsed now. Adds tests for plain, two-part, +security.N and .postN versions, and for out-of-range ones. Co-Authored-By: Claude --- requests/__init__.py | 5 +++++ tests/test_check_compatibility.py | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/test_check_compatibility.py diff --git a/requests/__init__.py b/requests/__init__.py index ab56c206d5..e1ebdf83e9 100644 --- a/requests/__init__.py +++ b/requests/__init__.py @@ -62,6 +62,11 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver urllib3_version = urllib3_version.split('+', 1)[0].split('.') assert urllib3_version != ['dev'] # Verify urllib3 isn't installed from git. + # Only major.minor.patch matter. Drop a PEP 440 post-release segment such + # as the ActiveState '1.26.20.post4' builds, which would otherwise break + # the three-way unpack below. + urllib3_version = urllib3_version[:3] + # Sometimes, urllib3 only reports its version as 16.1. if len(urllib3_version) == 2: urllib3_version.append('0') diff --git a/tests/test_check_compatibility.py b/tests/test_check_compatibility.py new file mode 100644 index 0000000000..c93df6a9bc --- /dev/null +++ b/tests/test_check_compatibility.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""check_compatibility must accept the version strings that ActiveState's +urllib3 builds report: plain, PEP 440 local (+security.N) and post-release +(.postN).""" + +import pytest + +from requests import check_compatibility + + +@pytest.mark.parametrize('urllib3_version', ( + '1.26.20', + '1.26', + '1.26.20+security.2', + '1.26.20.post3', + '1.26.20.post4', + '1.21.1', +)) +def test_supported_urllib3_versions(urllib3_version): + check_compatibility(urllib3_version, '3.0.4', None) + + +@pytest.mark.parametrize('urllib3_version', ('2.0.7', '1.20.0', '1.27.0.post1')) +def test_unsupported_urllib3_versions(urllib3_version): + with pytest.raises(AssertionError): + check_compatibility(urllib3_version, '3.0.4', None) From 6137a482f91d3c8f9a5b5d46d728059132a07d9c Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Thu, 5 Nov 2026 08:26:39 -0600 Subject: [PATCH 3/3] Release 2.27.1.post4: no_proxy hardening, urllib3 .postN support ActiveState release of requests 2.27.1 for Python 2.7 (inventory 2.27.1.4). Switches from the +security.N local version to .postN. Co-Authored-By: Claude --- HISTORY.md | 21 +++++++++++++++++++++ requests/__version__.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 4fda387fbe..aec56d70ae 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,27 @@ dev - \[Short description of non-trivial change.\] +2.27.1.post4 (2026-11-05) +------------------------- + +**Security hardening** + +- `no_proxy` entries now match whole domain labels only: + `no_proxy=example.com` still bypasses the proxy for `example.com` and + `sub.example.com`, but no longer for `badexample.com`. A leading dot on an + entry is ignored. Backport of requests #7427 (2.34.0), the port of CPython + bpo-39057. No CVE or GHSA. Python 2 compatible. + +**Bugfix** + +- `check_compatibility()` accepts urllib3 post-release versions such as the + ActiveState `1.26.20.post4` build. It used to raise ValueError on them, + which `import requests` turned into a `RequestsDependencyWarning`. + +This release uses the `X.Y.Z.postN` version scheme, replacing the +`+security.N` local versions of earlier ActiveState releases (inventory +2.27.1.4). + 2.27.1+security.3 (2026-06-12) ------------------------------- diff --git a/requests/__version__.py b/requests/__version__.py index 0c956c86f7..eff6ce07b1 100644 --- a/requests/__version__.py +++ b/requests/__version__.py @@ -5,7 +5,7 @@ __title__ = 'requests' __description__ = 'Python HTTP for Humans.' __url__ = 'https://requests.readthedocs.io' -__version__ = '2.27.1+security.3' +__version__ = '2.27.1.post4' __build__ = 0x022701 __author__ = 'Kenneth Reitz' __author_email__ = 'me@kennethreitz.org'