Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
50e5a0e
[3.11] gh-87720 Fix additional header refolding-quoting edge case (GH…
miss-islington Oct 25, 2026
f3c1882
[3.11] gh-159428: Don't read mimetypes.knownfiles by default on Windo…
cjames23 Oct 25, 2026
95dadd7
[3.11] gh-158876: Update bundled Expat to 2.9.0 (GH-158881) (#160261)
StanFromIreland Oct 25, 2026
cd2a617
[3.11] gh-160300: Reject cookies with a negative version in http.cook…
miss-islington Oct 25, 2026
f2435de
[3.11] gh-160097: Reject links replacing the destination in `tarfile.…
miss-islington Oct 25, 2026
efd5bb6
[3.11] Add Sphinx markup for CVEs in Changelog entries (GH-159415) (#…
StanFromIreland Oct 25, 2026
2691076
[3.11] gh-160040: Distrust compromised eMudhra intermediate CA keys (…
gpshead Oct 24, 2026
0be665c
[3.11] gh-160040: Add ssl eMudhra distrust entry to whatsnew (3.10.22…
tiran Oct 24, 2026
b563ed8
gh-159894: Upgrade bundled pip to 26.2.2 (3.10.22.x)
icanhasmath Oct 27, 2026
bad5432
Update version to 3.10.22.1 and add release notes
icanhasmath Oct 27, 2026
4fa495e
[3.11] Fix Windows build regen with Python 3.15 as the host Python (#…
ambv Oct 24, 2026
c1d1d62
[3.13] gh-143921: Reject NUL, CR and LF in IMAP commands (GH-143922, …
serhiy-storchaka Jul 7, 2026
acaf81f
gh-143923: Reject control characters in POP3 commands
sethmlarson Jan 20, 2026
833a27a
gh-150751: validate http.client Content-Length and chunk-size (GH-150…
metsw24-max Oct 8, 2026
dbefda3
gh-150751: Ignore optional whitespace around Content-Length in http.c…
danilablagorodniy37 Oct 14, 2026
0db2c2d
Release notes: imaplib, poplib and http.client fixes in 3.10.22.1
icanhasmath Oct 28, 2026
580267d
Upgrade bundled setuptools to 84.2.1 (3.10.22.x)
icanhasmath Oct 28, 2026
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
8 changes: 7 additions & 1 deletion Doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

extensions = ['sphinx.ext.coverage', 'sphinx.ext.doctest',
'pyspecific', 'c_annotations', 'escape4chm',
'asdl_highlight', 'peg_highlight', 'glossary_search']
'asdl_highlight', 'peg_highlight', 'glossary_search',
'sphinx.ext.extlinks']

doctest_global_setup = '''
try:
Expand Down Expand Up @@ -234,3 +235,8 @@
# Relative filename of the data files
refcount_file = 'data/refcounts.dat'
stable_abi_file = 'data/stable_abi.dat'

# sphinx.ext.extlinks config
extlinks = {
'cve': ('https://www.cve.org/CVERecord?id=CVE-%s', 'CVE-%s'),
}
5 changes: 5 additions & 0 deletions Doc/library/tarfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,11 @@ reused in custom filters:
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
path (after following symlinks) would end up outside the destination.
This raises :class:`~tarfile.OutsideDestinationError`.
- :ref:`Refuse <tarfile-extraction-refuse>` to extract links (hard or soft)
whose name refers to the destination directory itself.
This raises :class:`~tarfile.OutsideDestinationError`.
Note that such files are refused even on platforms that do not support
symbolic links.
- Clear high mode bits (setuid, setgid, sticky) and group/other write bits
(:attr:`~stat.S_IWGRP`|:attr:`~stat.S_IWOTH`).

Expand Down
2 changes: 2 additions & 0 deletions Doc/tools/extensions/pyspecific.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ def run(self):

issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)', re.I)
gh_issue_re = re.compile('(?:gh-issue-|gh-)([0-9]+)', re.I)
cve_re = re.compile('CVE-([0-9]{4}-[0-9]{4,})', re.I)
whatsnew_re = re.compile(r"(?im)^what's new in (.*?)\??$")


Expand Down Expand Up @@ -467,6 +468,7 @@ def run(self):
content = issue_re.sub(r':issue:`\1`', content)
# Fallback handling for the GitHub issue
content = gh_issue_re.sub(r':gh:`\1`', content)
content = cve_re.sub(r':cve:`\1`', content)
content = whatsnew_re.sub(r'\1', content)
# remove first 3 lines as they are the main heading
lines = ['.. default-role:: obj', ''] + content.splitlines()[3:]
Expand Down
66 changes: 66 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2428,3 +2428,69 @@ tarfile
:func:`~tarfile.TarFile.errorlevel` is zero.
(Contributed by Matt Prodani and Petr Viktorin in :gh:`112887`
and CVE 2025-4435.)


Notable changes in 3.10.22.1
============================

This is an ActiveState security release of the 3.10 line, which is past
its upstream end of life.

It carries the security fixes from upstream Python 3.11.18 (released
2026-10-25) that apply to 3.10, on top of 3.10.22, plus three fixes that
upstream released only for newer lines (or has not released yet): the
:mod:`imaplib`, :mod:`poplib` and :mod:`http.client` items below. The full
list of changes is in the changelog; the notable ones are below.

ssl
---

* The :mod:`ssl` module now rejects TLS certificate chains that contain one
of a set of compromised eMudhra intermediate CA certificates. The affected
certificates are identified by their public key, so cross-signed copies that
reuse the same key are blocked as well. It is a defense-in-depth backstop
that applies even when an application supplies its own root CA bundle, and
regardless of whether the platform trust store has removed trust.
(Contributed by Christian Heimes in :gh:`160040` for CVE 2026-115557.)

ensurepip
---------

* The bundled ``pip`` is upgraded from 23.0.1 to 26.2.2. pip loads its
own vendored ``certifi`` CA bundle in addition to the system trust store,
and the bundle in pip 23.0.1 still trusts the compromised eMudhra emSign
root; pip 26.2.2 ships certifi 2026.10.19, which removes it.
(:gh:`159894`)
* The bundled ``setuptools`` is upgraded from 79.0.1 to 84.2.1, fixing
CVE 2026-59890 and GHSA-xmj3-9gf7-h52w. setuptools removed
``pkg_resources`` in 82.0.0, so environments seeded by :mod:`ensurepip` or
:mod:`venv` no longer include it; install an older setuptools if an
application still imports it.

tarfile
-------

* :func:`tarfile.tar_filter` now rejects link members whose path resolves to
the destination directory itself, as :func:`tarfile.data_filter` already
did. (:gh:`160097`, CVE 2026-114583.)

Other security fixes
--------------------

* :class:`http.cookiejar.DefaultCookiePolicy` no longer accepts cookies with
a negative ``Version`` attribute without checking their domain.
(:gh:`160300`)
* :mod:`mimetypes` no longer reads :data:`mimetypes.knownfiles` by default on
Windows, where those paths resolve to the root of the current drive.
(:gh:`159428`)
* The bundled libexpat is updated to 2.9.0. (:gh:`158876`)
* :mod:`email` header refolding now re-quotes text that came from an
encoded word. (:gh:`87720`)
* :mod:`imaplib` rejects NUL, CR and LF in command arguments, which could
be used to inject extra IMAP commands. (:gh:`143921`, CVE 2025-15366.)
* :mod:`poplib` rejects control characters in commands, which could be used
to inject extra POP3 commands. (:gh:`143923`, CVE 2025-15367.)
* :mod:`http.client` only accepts a ``Content-Length`` and chunk sizes that
match the HTTP grammar (digits, and hex digits for chunk sizes); signs,
underscores and ``0x`` prefixes are no longer accepted.
(:gh:`150751`, CVE 2026-103995.)
2 changes: 1 addition & 1 deletion Include/patchlevel.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#define PY_RELEASE_SERIAL 0

/* Version as a string */
#define PY_VERSION "3.10.22"
#define PY_VERSION "3.10.22.1"
/*--end constants--*/

/* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2.
Expand Down
13 changes: 12 additions & 1 deletion Lib/email/_header_value_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ def comments(self):
comments.extend(token.comments)
return comments

def contains_token_type(self, *token_types):
if self.token_type in token_types:
return True
for t in self:
if t.contains_token_type(*token_types):
return True
return False

def fold(self, *, policy):
return _refold_parse_tree(self, policy=policy)

Expand Down Expand Up @@ -922,6 +930,9 @@ def pop_trailing_ws(self):
def comments(self):
return []

def contains_token_type(self, *token_types):
return self.token_type in token_types

def __getnewargs__(self):
return(str(self), self.token_type)

Expand Down Expand Up @@ -2795,7 +2806,7 @@ def _refold_parse_tree(parse_tree, *, policy):
continue
tstr = str(part)
if not want_encoding:
if part.token_type in ('ptext', 'vtext'):
if part.token_type == 'ptext' or part.contains_token_type('vtext'):
# Encode if tstr contains special characters.
want_encoding = not SPECIALSNL.isdisjoint(tstr)
else:
Expand Down
4 changes: 2 additions & 2 deletions Lib/ensurepip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

__all__ = ["version", "bootstrap"]
_PACKAGE_NAMES = ('setuptools', 'pip')
_SETUPTOOLS_VERSION = "79.0.1"
_PIP_VERSION = "23.0.1"
_SETUPTOOLS_VERSION = "84.2.1"
_PIP_VERSION = "26.2.2"
_PROJECTS = [
("setuptools", _SETUPTOOLS_VERSION, "py3"),
("pip", _PIP_VERSION, "py3"),
Expand Down
Binary file removed Lib/ensurepip/_bundled/pip-23.0.1-py3-none-any.whl
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
22 changes: 14 additions & 8 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@
# to prevent http header injection.
_contains_disallowed_method_pchar_re = re.compile('[\x00-\x1f]')

# RFC 9112: Content-Length = 1*DIGIT and chunk-size = 1*HEXDIG.
# int() is more permissive, so we match against the grammar before calling it.
_is_legal_content_length = re.compile(r'[0-9]+').fullmatch
_is_legal_chunk_size = re.compile(rb'[0-9a-fA-F]+').fullmatch

# We always set the Content-Length header for these methods because some
# servers will otherwise respond with a 411
_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'}
Expand Down Expand Up @@ -370,14 +375,12 @@ def begin(self):
# NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
self.length = None
length = self.headers.get("content-length")
if length and not self.chunked:
try:
self.length = int(length)
except ValueError:
self.length = None
else:
if self.length < 0: # ignore nonsensical negative lengths
self.length = None
if length is not None:
# RFC 9112, section 5.1: optional whitespace (SP / HTAB) around
# the field value is not part of the value.
length = length.strip(' \t')
if length and not self.chunked and _is_legal_content_length(length):
self.length = int(length)
else:
self.length = None

Expand Down Expand Up @@ -544,7 +547,10 @@ def _read_next_chunk_size(self):
i = line.find(b";")
if i >= 0:
line = line[:i] # strip chunk-extensions
line = line.rstrip()
try:
if not _is_legal_chunk_size(line):
raise ValueError("invalid chunk size")
return int(line, 16)
except ValueError:
# close the connection as protocol synchronisation is
Expand Down
11 changes: 11 additions & 0 deletions Lib/http/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,12 @@ def set_ok_version(self, cookie, request):
_debug(" Set-Cookie2 without version attribute (%s=%s)",
cookie.name, cookie.value)
return False
if cookie.version < 0:
# Neither the Netscape (version 0) nor the RFC 2965 (version > 0)
# rules would apply, so the domain checks would all be skipped.
_debug(" negative version attribute (%s=%s)",
cookie.name, cookie.value)
return False
if cookie.version > 0 and not self.rfc2965:
_debug(" RFC 2965 cookies are switched off")
return False
Expand Down Expand Up @@ -1112,6 +1118,9 @@ def return_ok(self, cookie, request):
return True

def return_ok_version(self, cookie, request):
if cookie.version < 0:
_debug(" negative version attribute")
return False
if cookie.version > 0 and not self.rfc2965:
_debug(" RFC 2965 cookies are switched off")
return False
Expand Down Expand Up @@ -1505,6 +1514,8 @@ def _cookie_from_cookie_tuple(self, tup, request):
version = int(version)
except ValueError:
return None # invalid version, ignore cookie
if version < 0:
return None # invalid version, ignore cookie
secure = standard.get("secure", False)
# (discard is also set if expires is Absent)
discard = standard.get("discard", False)
Expand Down
6 changes: 5 additions & 1 deletion Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@
# We compile these in _mode_xxx.
_Literal = br'.*{(?P<size>\d+)}$'
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'

# Only NUL, CR and LF are unsafe (they cannot be represented even in
# a quoted string, and CR/LF would end the command line early).
_control_chars = re.compile(b'[\x00\r\n]')


class IMAP4:
Expand Down Expand Up @@ -994,6 +996,8 @@ def _command(self, name, *args):
if arg is None: continue
if isinstance(arg, str):
arg = bytes(arg, self._encoding)
if _control_chars.search(arg):
raise ValueError("NUL, CR and LF not allowed in commands")
data = data + b' ' + arg

literal = self.literal
Expand Down
28 changes: 17 additions & 11 deletions Lib/mimetypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,23 @@
"suffix_map", "encodings_map", "types_map", "common_types"
]

knownfiles = [
"/etc/mime.types",
"/etc/httpd/mime.types", # Mac OS X
"/etc/httpd/conf/mime.types", # Apache
"/etc/apache/mime.types", # Apache 1
"/etc/apache2/mime.types", # Apache 2
"/usr/local/etc/httpd/conf/mime.types",
"/usr/local/lib/netscape/mime.types",
"/usr/local/etc/httpd/conf/mime.types", # Apache 1.2
"/usr/local/etc/mime.types", # Apache 1.3
]
if sys.platform == "win32":
# The default paths would resolve to the root of the current drive on Windows
# (e.g. C:\etc\mime.types), which unprivileged users can usually create.
# Only the registry is used by default.
knownfiles = []
else:
knownfiles = [
"/etc/mime.types",
"/etc/httpd/mime.types", # Mac OS X
"/etc/httpd/conf/mime.types", # Apache
"/etc/apache/mime.types", # Apache 1
"/etc/apache2/mime.types", # Apache 2
"/usr/local/etc/httpd/conf/mime.types",
"/usr/local/lib/netscape/mime.types",
"/usr/local/etc/httpd/conf/mime.types", # Apache 1.2
"/usr/local/etc/mime.types", # Apache 1.3
]

inited = False
_db = None
Expand Down
2 changes: 2 additions & 0 deletions Lib/poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ def _putline(self, line):
def _putcmd(self, line):
if self._debugging: print('*cmd*', repr(line))
line = bytes(line, self.encoding)
if re.search(b'[\x00-\x1F\x7F]', line):
raise ValueError('Control characters not allowed in commands')
self._putline(line)


Expand Down
11 changes: 6 additions & 5 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,12 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
strict=os.path.ALLOW_MISSING)
if os.path.commonpath([target_path, dest_path]) != dest_path:
raise OutsideDestinationError(member, target_path)
# A link member named after the destination directory itself
# would replace it with a (sym)link, redirecting the destination
# for all subsequent members.
if ((member.islnk() or member.issym())
and os.path.normpath(name) == os.curdir):
raise OutsideDestinationError(member, target_path)
# Limit permissions (no high bits, and go-w)
mode = member.mode
if mode is not None:
Expand Down Expand Up @@ -823,11 +829,6 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
if member.islnk() or member.issym():
if os.path.isabs(member.linkname):
raise AbsoluteLinkError(member)
# A link member that resolves to the destination directory itself
# would replace it with a (sym)link, redirecting the destination
# for all subsequent members.
if target_path == dest_path:
raise OutsideDestinationError(member, target_path)
normalized = os.path.normpath(member.linkname)
if normalized != member.linkname:
new_attrs['linkname'] = normalized
Expand Down
Binary file added Lib/test/distrusted.zip
Binary file not shown.
Loading
Loading