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
51 changes: 51 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,57 @@

.. towncrier release notes start

24.0.post4 (2026-10-29)
=======================

ActiveState security release for the Python 3.7-compatible pip 24.0 line.

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

- Upgrade vendored urllib3 from 1.26.17 to 1.26.20.post4, ActiveState's
security release of urllib3 1.26.20, with pip's usual vendoring changes
(Brotli support disabled). This is the same vendored urllib3 as pip
26.0.1.post3. It fixes:

- **CVE-2023-45803**: request bodies were not removed when a redirect
turned the request into a GET (fixed upstream in 1.26.18). (`GHSA-g4mx-q9vg-27p4 <https://github.com/advisories/GHSA-g4mx-q9vg-27p4>`_)
- **CVE-2024-37891**: the ``Proxy-Authorization`` header was kept on
cross-origin redirects (fixed upstream in 1.26.19). (`GHSA-34jh-p97f-mpxf <https://github.com/advisories/GHSA-34jh-p97f-mpxf>`_)
- **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-2024-47081**: Backport the requests 2.32.4 fix to the vendored
requests 2.31.0. ``requests.utils.get_netrc_auth()`` took the host name by
splitting the URL's netloc at the first ``:``, so a URL with credentials
in it could make it look up (and send) ``.netrc`` credentials for a
different host. It now uses the parsed host name. pip calls it for index
and find-links URLs. (`GHSA-9hjg-9r4m-mvj7 <https://github.com/advisories/GHSA-9hjg-9r4m-mvj7>`_)
- **CVE-2026-25645**: Backport the requests 2.33.0 fixes to the vendored
requests 2.31.0. ``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>`_)
- **CVE-2024-35195** is not ported. In requests before 2.32.0, a connection
pool first used with ``verify=False`` was reused by later requests to the
same host with verification on. pip never mixes the two on one adapter:
``--trusted-host`` hosts are mounted on their own ``InsecureHTTPAdapter``
and every other request goes through the verifying adapter, so pip does
not reach this case. (`GHSA-9wx4-h78v-vm56 <https://github.com/advisories/GHSA-9wx4-h78v-vm56>`_)

24.0.post3 (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 typing import List, Optional

__version__ = "24.0.post3"
__version__ = "24.0.post4"


def main(args: Optional[List[str]] = 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 @@ -45,7 +45,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 @@ -255,7 +254,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
21 changes: 9 additions & 12 deletions src/pip/_vendor/requests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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 @@ -229,12 +230,7 @@ def get_netrc_auth(url, raise_errors=False):

ri = urlparse(url)

# Strip port numbers from netloc. This weird `if...encode`` dance is
# used for Python 3.2, which doesn't support unicode literals.
splitstr = b":"
if isinstance(url, str):
splitstr = splitstr.decode("ascii")
host = ri.netloc.split(splitstr)[0]
host = ri.hostname

try:
_netrc = netrc(netrc_path).authenticators(host)
Expand Down Expand Up @@ -288,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
18 changes: 18 additions & 0 deletions src/pip/_vendor/urllib3/_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,24 @@ def getlist(self, key, default=__marker):
else:
return vals[1:]

def _prepare_for_method_change(self):
"""
Remove content-specific header fields before changing the request
method to GET or HEAD according to RFC 9110, Section 15.4.
"""
content_specific_headers = [
"Content-Encoding",
"Content-Language",
"Content-Location",
"Content-Type",
"Content-Length",
"Digest",
"Last-Modified",
]
for header in content_specific_headers:
self.discard(header)
return self

# Backwards compatibility for httplib
getheaders = getlist
getallmatchingheaders = getlist
Expand Down
7 changes: 6 additions & 1 deletion 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
__version__ = "1.26.17"
# 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"
70 changes: 55 additions & 15 deletions src/pip/_vendor/urllib3/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class BrokenPipeError(Exception):

# When it comes time to update this value as a part of regular maintenance
# (ie test_recent_date is failing) update it to ~6 months before the current date.
RECENT_DATE = datetime.date(2022, 1, 1)
RECENT_DATE = datetime.date(2024, 1, 1)

_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")

Expand Down 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 @@ -437,7 +446,7 @@ def connect(self):
and self.ssl_version is None
and hasattr(self.sock, "version")
and self.sock.version() in {"TLSv1", "TLSv1.1"}
):
): # Defensive:
warnings.warn(
"Negotiating TLSv1/TLSv1.1 by default is deprecated "
"and will be disabled in urllib3 v2.0.0. Connecting to "
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
26 changes: 24 additions & 2 deletions src/pip/_vendor/urllib3/connectionpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from socket import error as SocketError
from socket import timeout as SocketTimeout

from ._collections import HTTPHeaderDict
from .connection import (
BaseSSLError,
BrokenPipeError,
Expand Down Expand Up @@ -422,12 +423,13 @@ def _make_request(
pass
except IOError as e:
# Python 2 and macOS/Linux
# EPIPE and ESHUTDOWN are BrokenPipeError on Python 2, and EPROTOTYPE is needed on macOS
# EPIPE and ESHUTDOWN are BrokenPipeError on Python 2, and EPROTOTYPE/ECONNRESET are needed on macOS
# https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/
if e.errno not in {
errno.EPIPE,
errno.ESHUTDOWN,
errno.EPROTOTYPE,
errno.ECONNRESET,
}:
raise

Expand Down Expand Up @@ -767,7 +769,9 @@ def _is_ssl_error_message_from_http_proxy(ssl_error):
# so we try to cover our bases here!
message = " ".join(re.split("[^a-z]", str(ssl_error).lower()))
return (
"wrong version number" in message or "unknown protocol" in message
"wrong version number" in message
or "unknown protocol" in message
or "record layer failure" in message
)

# Try to detect a common user error with proxies which is to
Expand Down Expand Up @@ -843,7 +847,25 @@ def _is_ssl_error_message_from_http_proxy(ssl_error):
redirect_location = redirect and response.get_redirect_location()
if redirect_location:
if response.status == 303:
# Change the method according to RFC 9110, Section 15.4.4.
method = "GET"
# And lose the body not to transfer anything sensitive.
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)
Expand Down
3 changes: 1 addition & 2 deletions src/pip/_vendor/urllib3/contrib/securetransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@
import threading
import weakref

from pip._vendor import six

from .. import util
from ..packages import six
from ..util.ssl_ import PROTOCOL_TLS_CLIENT
from ._securetransport.bindings import CoreFoundation, Security, SecurityConst
from ._securetransport.low_level import (
Expand Down
25 changes: 22 additions & 3 deletions src/pip/_vendor/urllib3/poolmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import functools
import logging

from ._collections import RecentlyUsedContainer
from ._collections import HTTPHeaderDict, RecentlyUsedContainer
from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme
from .exceptions import (
LocationValueError,
Expand Down 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 @@ -382,11 +398,14 @@ def urlopen(self, method, url, redirect=True, **kw):
# Support relative URLs for redirecting.
redirect_location = urljoin(url, redirect_location)

# RFC 7231, Section 6.4.4
if response.status == 303:
# Change the method according to RFC 9110, Section 15.4.4.
method = "GET"
# And lose the body not to transfer anything sensitive.
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