Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@

.. towncrier release notes start

26.0.1.post3 (2026-10-28)
=========================

ActiveState security release for the Python 3.9-compatible pip 26.0.1 line.

Security Fixes
--------------

- Upgrade vendored urllib3 from 1.26.20 to 1.26.20.post4, ActiveState's
security release of urllib3 1.26.20, with pip's usual vendoring changes
(Brotli support disabled). It fixes:

- **CVE-2025-66418**: unbounded ``Content-Encoding`` decoder chain. (`GHSA-gm62-xv2j-4w53 <https://github.com/advisories/GHSA-gm62-xv2j-4w53>`_)
- **CVE-2025-66471**: the streaming API fully decompressed highly
compressed data in one operation. (`GHSA-2xpw-w6gg-jr37 <https://github.com/advisories/GHSA-2xpw-w6gg-jr37>`_)
- **CVE-2026-21441**: redirect bodies were decompressed while draining. (`GHSA-38jv-5279-wg99 <https://github.com/advisories/GHSA-38jv-5279-wg99>`_)
- **CVE-2026-44431**: sensitive headers were forwarded on cross-origin
redirects. (`GHSA-qccp-gfcp-xxvc <https://github.com/advisories/GHSA-qccp-gfcp-xxvc>`_)
- **CVE-2026-97687**: the TLS connection to an HTTPS proxy used the
target's TLS settings. (`GHSA-8988-9cw3-xx77 <https://github.com/advisories/GHSA-8988-9cw3-xx77>`_)
- **CVE-2026-97689**: chunk-size and trailer lines were read without a
length limit. (`GHSA-vxq7-64xx-v4gw <https://github.com/advisories/GHSA-vxq7-64xx-v4gw>`_)
- **CVE-2025-50181**: ``retries`` at the ``PoolManager`` level did not
limit redirects. (`GHSA-pq67-6m6q-mj2v <https://github.com/advisories/GHSA-pq67-6m6q-mj2v>`_)

- **CVE-2026-25645**: Backport the requests 2.33.0 fixes to the vendored
requests 2.32.5. ``requests.utils.extract_zipped_paths()`` extracted a
zip member to a predictable file name in the shared temporary directory
and reused an existing file there; it now extracts to a new ``mkstemp``
file. ``HTTPAdapter.cert_verify()`` no longer calls it for the default CA
bundle (pip's vendored certifi already extracts its bundle to a real
file), as in upstream requests. pip did not reach the vulnerable path;
the fix is defense in depth. (`GHSA-gc5v-m9x4-r6x2 <https://github.com/advisories/GHSA-gc5v-m9x4-r6x2>`_)

26.0.1.post2 (2026-10-28)
=========================

Expand Down
2 changes: 1 addition & 1 deletion src/pip/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

__version__ = "26.0.1.post2"
__version__ = "26.0.1.post3"


def main(args: list[str] | None = None) -> int:
Expand Down
3 changes: 1 addition & 2 deletions src/pip/_vendor/requests/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
from .structures import CaseInsensitiveDict
from .utils import (
DEFAULT_CA_BUNDLE_PATH,
extract_zipped_paths,
get_auth_from_url,
get_encoding_from_headers,
prepend_scheme_if_needed,
Expand Down Expand Up @@ -297,7 +296,7 @@ def cert_verify(self, conn, url, verify, cert):
cert_loc = verify

if not cert_loc:
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
cert_loc = DEFAULT_CA_BUNDLE_PATH

if not cert_loc or not os.path.exists(cert_loc):
raise OSError(
Expand Down
14 changes: 8 additions & 6 deletions src/pip/_vendor/requests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@

NETRC_FILES = (".netrc", "_netrc")

# Certificate is extracted by certifi when needed.
DEFAULT_CA_BUNDLE_PATH = certs.where()

DEFAULT_PORTS = {"http": 80, "https": 443}
Expand Down Expand Up @@ -283,12 +284,13 @@ def extract_zipped_paths(path):
return path

# we have a valid zip archive and a valid member of that archive
tmp = tempfile.gettempdir()
extracted_path = os.path.join(tmp, member.split("/")[-1])
if not os.path.exists(extracted_path):
# use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition
with atomic_open(extracted_path) as file_handler:
file_handler.write(zip_file.read(member))
suffix = os.path.splitext(member.split("/")[-1])[-1]
fd, extracted_path = tempfile.mkstemp(suffix=suffix)
try:
os.write(fd, zip_file.read(member))
finally:
os.close(fd)

return extracted_path


Expand Down
5 changes: 5 additions & 0 deletions src/pip/_vendor/urllib3/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
# This file is protected via CODEOWNERS
# ActiveState security release. The package metadata carries the post-release
# (__package_version__, which setup.py reads), while __version__ stays
# "1.26.20": requests unpacks urllib3.__version__ as major.minor.patch and
# emits RequestsDependencyWarning for anything longer.
__version__ = "1.26.20"
__package_version__ = "1.26.20.post4"
66 changes: 53 additions & 13 deletions src/pip/_vendor/urllib3/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ def connect(self):
hostname = self.host
tls_in_tls = False

if self._is_forwarding_through_https_proxy():
# The TLS connection being made is the one to the HTTPS proxy,
# which forwards requests without a CONNECT tunnel. It must use
# the proxy's TLS policy: never the target's SNI, hostname or
# fingerprint assertions, client certificate, or a cert_reqs
# that would override proxy_ssl_context's verify_mode.
self.sock, self.is_verified = self._wrap_proxy_socket(self.host, conn)
return

if self._is_using_tunnel():
if self.tls_in_tls_required:
self.sock = conn = self._connect_tls_proxy(hostname, conn)
Expand Down Expand Up @@ -476,28 +485,60 @@ def connect(self):
or self.assert_fingerprint is not None
)

def _is_forwarding_through_https_proxy(self):
return (
self.proxy is not None
and self.proxy.scheme == "https"
and not self._is_using_tunnel()
)

def _connect_tls_proxy(self, hostname, conn):
"""
Establish a TLS connection to the proxy using the provided SSL context.
Establish a TLS connection to the proxy using proxy-specific policy.
"""
socket, verified = self._wrap_proxy_socket(hostname, conn)
if verified is not None:
self.proxy_is_verified = verified
return socket

def _wrap_proxy_socket(self, hostname, conn):
"""
Wrap ``conn`` in TLS to the proxy at ``hostname``.

Returns the socket and whether the proxy's certificate was verified,
or None for a user-provided proxy context (left for the context to
report, as before).
"""
proxy_config = self.proxy_config
ssl_context = proxy_config.ssl_context
if ssl_context:
# If the user provided a proxy context, we assume CA and client
# certificates have already been set
return ssl_wrap_socket(
# certificates have already been set. Its verification policy is
# used as-is.
socket = ssl_wrap_socket(
sock=conn,
server_hostname=hostname,
ssl_context=ssl_context,
)

ssl_context = create_proxy_ssl_context(
self.ssl_version,
self.cert_reqs,
self.ca_certs,
self.ca_cert_dir,
self.ca_cert_data,
)
if self._is_forwarding_through_https_proxy():
return socket, ssl_context.verify_mode == ssl.CERT_REQUIRED
return socket, None

if self._is_forwarding_through_https_proxy() and self.ssl_context is not None:
# Forwarding with no proxy_ssl_context: for backwards
# compatibility the connection's ssl_context, with the pool's
# certificate policy, is used for the proxy handshake, as it was
# before proxy and target TLS settings were separated.
ssl_context = self.ssl_context
ssl_context.verify_mode = resolve_cert_reqs(self.cert_reqs)
else:
ssl_context = create_proxy_ssl_context(
self.ssl_version,
self.cert_reqs,
self.ca_certs,
self.ca_cert_dir,
self.ca_cert_data,
)

# If no cert was provided, use only the default options for server
# certificate validation
Expand Down Expand Up @@ -529,8 +570,7 @@ def _connect_tls_proxy(self, hostname, conn):
)
_match_hostname(cert, hostname)

self.proxy_is_verified = ssl_context.verify_mode == ssl.CERT_REQUIRED
return socket
return socket, ssl_context.verify_mode == ssl.CERT_REQUIRED


def _match_hostname(cert, asserted_hostname):
Expand Down
14 changes: 14 additions & 0 deletions src/pip/_vendor/urllib3/connectionpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,20 @@ def _is_ssl_error_message_from_http_proxy(ssl_error):
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 (
headers
and retries.remove_headers_on_redirect
and not self.is_same_host(redirect_location)
):
new_headers = headers.copy()
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:
Expand Down
18 changes: 17 additions & 1 deletion src/pip/_vendor/urllib3/poolmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,22 @@ class PoolManager(RequestMethods):

def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
RequestMethods.__init__(self, headers)
if "retries" in connection_pool_kw:
retries = connection_pool_kw["retries"]
if not isinstance(retries, Retry):
# When Retry is initialized, raise_on_redirect is based
# on a redirect boolean value.
# But requests made via a pool manager always set
# redirect to False, and raise_on_redirect always ends
# up being False consequently.
# Here we fix the issue by setting raise_on_redirect to
# a value needed by the pool manager without considering
# the redirect boolean.
raise_on_redirect = retries is not False
retries = Retry.from_int(retries, redirect=False)
retries.raise_on_redirect = raise_on_redirect
connection_pool_kw = connection_pool_kw.copy()
connection_pool_kw["retries"] = retries
self.connection_pool_kw = connection_pool_kw
self.pools = RecentlyUsedContainer(num_pools)

Expand Down Expand Up @@ -389,7 +405,7 @@ def urlopen(self, method, url, redirect=True, **kw):
kw["body"] = None
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()

retries = kw.get("retries")
retries = kw.get("retries", response.retries)
if not isinstance(retries, Retry):
retries = Retry.from_int(retries, redirect=redirect)

Expand Down
Loading
Loading