Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b20e2a5
Address CVE-2025-8194 (tarfile) and CVE-2026-4786 (webbrowser)
icanhasmath May 27, 2026
ec88ac6
Reject control characters in header/command APIs (injection cluster)
icanhasmath May 27, 2026
f4c3f15
Reject header injection when generating email messages (CVE-2024-6923)
icanhasmath May 27, 2026
72c20be
Fix Windows test-suite hangs on invalid file descriptors
icanhasmath Jun 8, 2026
392e389
asyncore: recognize WSAECONNRESET/WSAESHUTDOWN as disconnects on Windows
icanhasmath Jun 8, 2026
1a63015
test: gate test_bigrepeat on sys.maxsize, not sys.maxint (win64)
icanhasmath Jun 8, 2026
37f5972
test_socket: restore missing _have_socket_can/_have_socket_alg helpers
icanhasmath Jun 8, 2026
c54ba12
Harden zipfile against overlapping entries and bad ZIP64 locator
icanhasmath May 27, 2026
13d66c6
ctypes: fix find_msvcrt on VS2015+ builds; skip win64 pointer-truncat…
icanhasmath Jun 8, 2026
6e3990b
Reject misplaced square brackets in parsed URL hosts (CVE-2025-0938)
icanhasmath May 27, 2026
3b46f5d
ssl: accept WSAENOTCONN when probing whether a socket is connected
icanhasmath Jun 8, 2026
5fab2f3
Fix quadratic complexity in minidom and os.path.expandvars
icanhasmath May 27, 2026
8c9b9a1
test_socket: bind 'support' alias (partial 3.x backport)
icanhasmath Jun 9, 2026
8ee2195
Fix quadratic complexity in HTMLParser at EOF (CVE-2025-6069)
icanhasmath May 27, 2026
11825ac
test_ctypes: don't assert find_library("c") is non-None on UCRT
icanhasmath Jun 9, 2026
b2d9914
Add strict validation option to base64.b64decode
icanhasmath May 27, 2026
3d4ca65
test_ssl: accept WSAENOTCONN and skip TLS1.3/PHA tests on this build
icanhasmath Jun 9, 2026
8420333
Don't normalize AREGTYPE follow-up headers to DIRTYPE (CVE-2025-13462)
icanhasmath May 27, 2026
0c11934
test.support: add requires_linux_version (used by forward-ported tests)
icanhasmath Jun 9, 2026
f258e71
2.7.18.14 Release
icanhasmath May 28, 2026
584efeb
test_ftplib: add cmd_noop to dummy server; skip flaky source_address …
icanhasmath Jun 9, 2026
50cd81e
fix(tests): skip-guard expandvars nonascii test; restore _have_socket…
icanhasmath May 29, 2026
07c707f
2.7.18.14 Release: document Windows regression remediation
icanhasmath Jun 10, 2026
dc6c0ed
Merge branch '2.7.18.14' (bc3b091) into the Windows remediation release
icanhasmath Jun 10, 2026
a7d70a3
Harden header/host control-char validation: unicode values + trailing…
icanhasmath Jun 10, 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
2 changes: 1 addition & 1 deletion Include/patchlevel.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
#define PY_RELEASE_SERIAL 0

/* Version as a string */
#define PY_VERSION "2.7.18.13"
#define PY_VERSION "2.7.18.14"
/*--end constants--*/

/* Subversion Revision number of this file (not of the repository). Empty
Expand Down
23 changes: 19 additions & 4 deletions Lib/Cookie.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,14 @@
'Set-Cookie: chips=ahoy\r\nSet-Cookie: vienna=finger'

The load() method is darn-tootin smart about identifying cookies
within a string. Escaped quotation marks, nested semicolons, and other
such trickeries do not confuse it.
within a string. Escaped quotation marks and nested semicolons do not
confuse it. (Note that cookies whose values contain control characters
are now rejected to prevent Set-Cookie header injection; CVE-2026-0672.)

>>> C = Cookie.SmartCookie()
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=delicious;";')
>>> print C
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=delicious;"

Each element of the Cookie also supports all of the RFC 2109
Cookie attributes. Here's an example which sets the Path
Expand Down Expand Up @@ -242,6 +243,15 @@ class CookieError(Exception):
# _Translator hash-table for fast quoting
#
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~"
_control_character_re = re.compile(r'[\x00-\x1f\x7f]')

def _has_control_character(*values):
"""Return True if any of the given string values holds a control char."""
for v in values:
if isinstance(v, basestring) and _control_character_re.search(v):
return True
return False

_Translator = {
'\000' : '\\000', '\001' : '\\001', '\002' : '\\002',
'\003' : '\\003', '\004' : '\\004', '\005' : '\\005',
Expand Down Expand Up @@ -424,6 +434,8 @@ def __setitem__(self, K, V):
K = K.lower()
if not K in self._reserved:
raise CookieError("Invalid Attribute %s" % K)
if _has_control_character(K, V):
raise CookieError("Control characters are not allowed in cookies: %r %r" % (K, V))
dict.__setitem__(self, K, V)
# end __setitem__

Expand All @@ -440,6 +452,9 @@ def set(self, key, val, coded_val,
raise CookieError("Attempt to set a reserved key: %s" % key)
if "" != translate(key, idmap, LegalChars):
raise CookieError("Illegal key value: %s" % key)
if _has_control_character(key, val, coded_val):
raise CookieError("Control characters are not allowed in cookies: %r %r %r"
% (key, val, coded_val))

# It's a good key, so save it.
self.key = key
Expand Down
41 changes: 33 additions & 8 deletions Lib/HTMLParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]')

starttagopen = re.compile('<[a-zA-Z]')
endtagopen = re.compile('</[a-zA-Z]')
piclose = re.compile('>')
commentclose = re.compile(r'--\s*>')

Expand Down Expand Up @@ -167,22 +168,46 @@ def goahead(self, end):
k = self.parse_pi(i)
elif startswith("<!", i):
k = self.parse_html_declaration(i)
elif (i + 1) < n:
elif (i + 1) < n or end:
self.handle_data("<")
k = i + 1
else:
break
if k < 0:
if not end:
break
k = rawdata.find('>', i + 1)
if k < 0:
k = rawdata.find('<', i + 1)
if k < 0:
k = i + 1
# End of input with an unterminated construct. Close it
# per HTML5 instead of rescanning, which made repeated
# incomplete constructs quadratic (CVE-2025-6069).
if starttagopen.match(rawdata, i): # < + letter
pass
elif startswith("</", i):
if i + 2 == n:
self.handle_data("</")
elif endtagopen.match(rawdata, i): # </ + letter
pass
else:
# bogus comment
self.handle_comment(rawdata[i+2:])
elif startswith("<!--", i):
j = n
for suffix in ("--!", "--", "-"):
if rawdata.endswith(suffix, i+4):
j -= len(suffix)
break
self.handle_comment(rawdata[i+4:j])
elif startswith("<![CDATA[", i):
self.unknown_decl(rawdata[i+3:])
elif rawdata[i:i+9].lower() == '<!doctype':
self.handle_decl(rawdata[i+2:])
elif startswith("<!", i):
# bogus comment
self.handle_comment(rawdata[i+2:])
elif startswith("<?", i):
self.handle_pi(rawdata[i+2:])
else:
k += 1
self.handle_data(rawdata[i:k])
raise AssertionError("we should not get here!")
k = n
i = self.updatepos(i, k)
elif startswith("&#", i):
match = charref.match(rawdata, i)
Expand Down
7 changes: 5 additions & 2 deletions Lib/asyncore.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
# On Windows, handle the Windows error numbers
from errno import \
WSAEWOULDBLOCK, WSAENOTCONN, WSAEINPROGRESS, WSAEALREADY, WSAEISCONN, \
WSAECONNABORTED, WSAENOTCONN, WSAEBADF
WSAECONNABORTED, WSAECONNRESET, WSAESHUTDOWN, WSAEBADF
else:
# On Posix the error codes aren't duplicated, with different numbers
WSAEWOULDBLOCK = EWOULDBLOCK
Expand All @@ -70,12 +70,15 @@
WSAEALREADY = EALREADY
WSAEISCONN = EISCONN
WSAECONNABORTED = ECONNABORTED
WSAECONNRESET = ECONNRESET
WSAESHUTDOWN = ESHUTDOWN
WSAENOTCONN = ENOTCONN
WSAEBADF = EBADF


_DISCONNECTED = frozenset((ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE,
EBADF, WSAENOTCONN, WSAECONNABORTED, WSAEBADF))
EBADF, WSAECONNRESET, WSAENOTCONN, WSAESHUTDOWN,
WSAECONNABORTED, WSAEBADF))

try:
socket_map
Expand Down
27 changes: 23 additions & 4 deletions Lib/base64.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,37 @@ def b64encode(s, altchars=None):
return encoded


def b64decode(s, altchars=None):
def b64decode(s, altchars=None, validate=False):
"""Decode a Base64 encoded string.
s is the string to decode. Optional altchars must be a string of at least
length 2 (additional characters are ignored) which specifies the
alternative alphabet used instead of the '+' and '/' characters.
The decoded string is returned. A TypeError is raised if s is
incorrectly padded. Characters that are neither in the normal base-64
alphabet nor the alternative alphabet are discarded prior to the padding
check.
incorrectly padded.
If validate is False (the default), characters that are neither in the
normal base-64 alphabet nor the alternative alphabet are discarded prior
to the padding check. If validate is True, these non-alphabet characters
in the input result in a binascii.Error.
Unlike upstream (which only deprecates the lenient behaviour), validation
here checks the input against the *requested* alphabet, so the standard
'+'/'/' characters are rejected when an alternative alphabet is given
(CVE-2025-12781), and any data after the padding is rejected rather than
silently ignored (CVE-2026-3446).
"""
if validate:
if altchars is not None:
extra = altchars[:2]
else:
extra = b'+/'
valid = frozenset(string.ascii_letters + string.digits + extra)
stripped = s.rstrip(b'=')
npad = len(s) - len(stripped)
if npad > 2 or not all(c in valid for c in stripped):
raise binascii.Error('Non-base64 digit found')
if altchars is not None:
s = s.translate(string.maketrans(altchars[:2], '+/'))
try:
Expand Down
5 changes: 4 additions & 1 deletion Lib/ctypes/test/test_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ def test_find(self):
@unittest.skipUnless(os.name in ("nt", "ce"),
'test specific to Windows (NT/CE)')
def test_load_library(self):
self.assertIsNotNone(libc_name)
# libc_name (find_library("c")) is None on VS2015+/UCRT builds, where
# the C runtime is no longer loadable as a single DLL. That is expected
# (see the skipped tests above); this test really exercises loading
# kernel32, which is independent of libc_name.
if is_resource_enabled("printing"):
print find_library("kernel32")
print find_library("user32")
Expand Down
2 changes: 2 additions & 0 deletions Lib/ctypes/test/test_pointers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class A(POINTER(c_ulong)):
# Pointer can't set contents: has no _type_
self.assertRaises(TypeError, A, c_ulong(33))

@unittest.skipUnless(sizeof(c_void_p) == sizeof(c_long),
"test assumes c_long is pointer-sized (fails on win64)")
def test_pass_pointers(self):
dll = CDLL(_ctypes_test.__file__)
func = dll._testfunc_p_p
Expand Down
2 changes: 2 additions & 0 deletions Lib/ctypes/test/test_prototypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def test_paramflags(self):
self.assertEqual(func(input=None), None)


@unittest.skipUnless(sizeof(c_void_p) == sizeof(c_long),
"test assumes c_long is pointer-sized (fails on win64)")
def test_int_pointer_arg(self):
func = testdll._testfunc_p_p
func.restype = c_long
Expand Down
9 changes: 8 additions & 1 deletion Lib/ctypes/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ def _get_build_version():
i = i + len(prefix)
s, rest = sys.version[i:].split(" ", 1)
majorVersion = int(s[:-2]) - 6
if majorVersion >= 13:
# v13 was skipped and should be v14
majorVersion += 1
minorVersion = int(s[2:3]) / 10.0
# I don't think paths are affected by minor version in version 6
if majorVersion == 6:
Expand All @@ -36,8 +39,12 @@ def find_msvcrt():
return None
if version <= 6:
clibname = 'msvcrt'
else:
elif version <= 13:
clibname = 'msvcr%d' % (version * 10)
else:
# CRT is no longer directly loadable. See issue23606 for the
# discussion about alternative approaches.
return None

# If python was built with in debug mode
import imp
Expand Down
4 changes: 4 additions & 0 deletions Lib/email/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class CharsetError(MessageError):
"""An illegal charset was given."""


class HeaderWriteError(MessageError):
"""Error while writing headers."""



# These are parsing defects which the parser was able to work around.
class MessageDefect:
Expand Down
25 changes: 20 additions & 5 deletions Lib/email/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@

from cStringIO import StringIO
from email.header import Header
from email.errors import HeaderWriteError

# Matches a CR/LF that is NOT part of a valid header folding (i.e. not
# immediately followed by folding whitespace). Used to detect injected
# newlines in generated headers (CVE-2024-6923). The negative lookaheads
# (rather than consuming character classes) also fire at end-of-string, so a
# value ending in a bare CR/LF/CRLF -- which would otherwise be written out as
# an injected line break -- is rejected too.
NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n(?![ \t])|\r(?![ \n\t])|\n(?![ \t])')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A negative look ahead is much more complex to execute than multiple branches. Can this instead just keep the original [^ \t] etc, but add |$ to address the original comment?
Also, speaking of inefficiencies, the first branch here it totally redundant as the third branch will also match all the same strings.


UNDERSCORE = '_'
NL = '\n'
Expand Down Expand Up @@ -139,29 +148,35 @@ def _dispatch(self, msg):

def _write_headers(self, msg):
for h, v in msg.items():
print >> self._fp, '%s:' % h,
if self._maxheaderlen == 0:
# Explicit no-wrapping
print >> self._fp, v
value = v
elif isinstance(v, Header):
# Header instances know what to do
print >> self._fp, v.encode()
value = v.encode()
elif _is8bitstring(v):
# If we have raw 8bit data in a byte string, we have no idea
# what the encoding is. There is no safe way to split this
# string. If it's ascii-subset, then we could do a normal
# ascii split, but if it's multibyte then we could break the
# string. There's no way to know so the least harm seems to
# be to not split the string and risk it being too long.
print >> self._fp, v
value = v
else:
# Header's got lots of smarts, so use it. Note that this is
# fundamentally broken though because we lose idempotency when
# the header string is continued with tabs. It will now be
# continued with spaces. This was reversedly broken before we
# fixed bug 1974. Either way, we lose.
print >> self._fp, Header(
value = Header(
v, maxlinelen=self._maxheaderlen, header_name=h).encode()
# Reject headers that contain an injected newline, i.e. a CR/LF
# that is not part of valid header folding (CVE-2024-6923).
folded = '%s: %s' % (h, value)
if NEWLINE_WITHOUT_FWSP.search(folded):
raise HeaderWriteError(
"header value contains an unexpected newline: %r" % (folded,))
print >> self._fp, folded
# A blank line always separates headers from body
print >> self._fp

Expand Down
26 changes: 26 additions & 0 deletions Lib/email/test/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,32 @@ def _msgobj(self, filename):

# Test various aspects of the Message class's API
class TestMessageAPI(TestEmailBase):
def test_string_rejects_header_injection(self):
# CVE-2024-6923: generating a message must reject header values that
# contain an injected newline (one not part of valid folding). The
# no-wrap path writes the value verbatim, which deterministically
# exercises the check.
import email.errors
from email.generator import Generator
from cStringIO import StringIO
for bad in ('value\r\nInjected: header',
'value\nInjected: header',
'value\rstuff',
# A bare CR/LF/CRLF at end-of-string is also an injected
# newline: the generator appends its own newline after it,
# prematurely terminating the header block.
'value\n',
'value\r',
'value\r\n'):
msg = Message()
msg['Subject'] = bad
g = Generator(StringIO(), maxheaderlen=0)
self.assertRaises(email.errors.HeaderWriteError, g.flatten, msg)
# A normal header is still emitted fine.
msg = Message()
msg['Subject'] = 'a normal subject that is reasonably short'
self.assertIn('Subject: a normal subject', msg.as_string())

def test_get_all(self):
eq = self.assertEqual
msg = self._msgobj('msg_20.txt')
Expand Down
3 changes: 3 additions & 0 deletions Lib/httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,9 @@ def set_tunnel(self, host, port=None, headers=None):
if self.sock:
raise RuntimeError("Can't setup tunnel for established connection.")

# Reject control characters (notably CR/LF) in the tunnel host so they
# cannot be injected into the CONNECT request line (CVE-2026-1502).
self._validate_host(host)
self._tunnel_host, self._tunnel_port = self._get_hostport(host, port)
if headers:
self._tunnel_headers = headers
Expand Down
5 changes: 5 additions & 0 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
# Control characters (C0 and DEL) must not appear in command arguments;
# otherwise CR/LF could be used to inject extra IMAP commands (CVE-2025-15366).
_control_chars = re.compile(r'[\x00-\x1f\x7f]')



Expand Down Expand Up @@ -852,6 +855,8 @@ def _command(self, name, *args):
data = '%s %s' % (tag, name)
for arg in args:
if arg is None: continue
if isinstance(arg, basestring) and _control_chars.search(arg):
raise ValueError("Control characters not allowed in commands")
data = '%s %s' % (data, self._checkquote(arg))

literal = self.literal
Expand Down
Loading