Compare commits

...

No commits in common. "c8" and "c8s" have entirely different histories.
c8 ... c8s

8 changed files with 1 additions and 6133 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,223 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
<31488909+miss-islington@users.noreply.github.com>
Date: Mon, 22 May 2023 03:42:37 -0700
Subject: [PATCH] 00399: CVE-2023-24329
gh-102153: Start stripping C0 control and space chars in `urlsplit` (GH-102508)
`urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit GH-25595.
This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/GH-url-parsing:~:text=Remove%20any%20leading%20and%20trailing%20C0%20control%20or%20space%20from%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
Backported from Python 3.12
(cherry picked from commit f48a96a28012d28ae37a2f4587a780a5eb779946)
Co-authored-by: Illia Volochii <illia.volochii@gmail.com>
Co-authored-by: Gregory P. Smith [Google] <greg@krypto.org>
---
Doc/library/urllib.parse.rst | 40 +++++++++++-
Lib/test/test_urlparse.py | 61 ++++++++++++++++++-
Lib/urllib/parse.py | 12 ++++
...-03-07-20-59-17.gh-issue-102153.14CLSZ.rst | 3 +
4 files changed, 113 insertions(+), 3 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst
index b717d7cc05..83a7a82089 100644
--- a/Doc/library/urllib.parse.rst
+++ b/Doc/library/urllib.parse.rst
@@ -126,6 +126,12 @@ or on combining URL components into a URL string.
``#``, ``@``, or ``:`` will raise a :exc:`ValueError`. If the URL is
decomposed before parsing, no error will be raised.
+
+ .. warning::
+
+ :func:`urlparse` does not perform validation. See :ref:`URL parsing
+ security <url-parsing-security>` for details.
+
.. versionchanged:: 3.2
Added IPv6 URL parsing capabilities.
@@ -288,8 +294,14 @@ or on combining URL components into a URL string.
``#``, ``@``, or ``:`` will raise a :exc:`ValueError`. If the URL is
decomposed before parsing, no error will be raised.
- Following the `WHATWG spec`_ that updates RFC 3986, ASCII newline
- ``\n``, ``\r`` and tab ``\t`` characters are stripped from the URL.
+ Following some of the `WHATWG spec`_ that updates RFC 3986, leading C0
+ control and space characters are stripped from the URL. ``\n``,
+ ``\r`` and tab ``\t`` characters are removed from the URL at any position.
+
+ .. warning::
+
+ :func:`urlsplit` does not perform validation. See :ref:`URL parsing
+ security <url-parsing-security>` for details.
.. versionchanged:: 3.6
Out-of-range port numbers now raise :exc:`ValueError`, instead of
@@ -302,6 +314,9 @@ or on combining URL components into a URL string.
.. versionchanged:: 3.6.14
ASCII newline and tab characters are stripped from the URL.
+ .. versionchanged:: 3.6.15
+ Leading WHATWG C0 control and space characters are stripped from the URL.
+
.. _WHATWG spec: https://url.spec.whatwg.org/#concept-basic-url-parser
.. function:: urlunsplit(parts)
@@ -371,6 +386,27 @@ or on combining URL components into a URL string.
.. versionchanged:: 3.2
Result is a structured object rather than a simple 2-tuple.
+.. _url-parsing-security:
+
+URL parsing security
+--------------------
+
+The :func:`urlsplit` and :func:`urlparse` APIs do not perform **validation** of
+inputs. They may not raise errors on inputs that other applications consider
+invalid. They may also succeed on some inputs that might not be considered
+URLs elsewhere. Their purpose is for practical functionality rather than
+purity.
+
+Instead of raising an exception on unusual input, they may instead return some
+component parts as empty strings. Or components may contain more than perhaps
+they should.
+
+We recommend that users of these APIs where the values may be used anywhere
+with security implications code defensively. Do some verification within your
+code before trusting a returned component part. Does that ``scheme`` make
+sense? Is that a sensible ``path``? Is there anything strange about that
+``hostname``? etc.
+
.. _parsing-ascii-encoded-bytes:
Parsing ASCII Encoded Bytes
diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
index 3509278a01..7fd61ffea9 100644
--- a/Lib/test/test_urlparse.py
+++ b/Lib/test/test_urlparse.py
@@ -660,6 +660,65 @@ class UrlParseTestCase(unittest.TestCase):
self.assertEqual(p.scheme, "https")
self.assertEqual(p.geturl(), "https://www.python.org/javascript:alert('msg')/?query=something#fragment")
+ def test_urlsplit_strip_url(self):
+ noise = bytes(range(0, 0x20 + 1))
+ base_url = "http://User:Pass@www.python.org:080/doc/?query=yes#frag"
+
+ url = noise.decode("utf-8") + base_url
+ p = urllib.parse.urlsplit(url)
+ self.assertEqual(p.scheme, "http")
+ self.assertEqual(p.netloc, "User:Pass@www.python.org:080")
+ self.assertEqual(p.path, "/doc/")
+ self.assertEqual(p.query, "query=yes")
+ self.assertEqual(p.fragment, "frag")
+ self.assertEqual(p.username, "User")
+ self.assertEqual(p.password, "Pass")
+ self.assertEqual(p.hostname, "www.python.org")
+ self.assertEqual(p.port, 80)
+ self.assertEqual(p.geturl(), base_url)
+
+ url = noise + base_url.encode("utf-8")
+ p = urllib.parse.urlsplit(url)
+ self.assertEqual(p.scheme, b"http")
+ self.assertEqual(p.netloc, b"User:Pass@www.python.org:080")
+ self.assertEqual(p.path, b"/doc/")
+ self.assertEqual(p.query, b"query=yes")
+ self.assertEqual(p.fragment, b"frag")
+ self.assertEqual(p.username, b"User")
+ self.assertEqual(p.password, b"Pass")
+ self.assertEqual(p.hostname, b"www.python.org")
+ self.assertEqual(p.port, 80)
+ self.assertEqual(p.geturl(), base_url.encode("utf-8"))
+
+ # Test that trailing space is preserved as some applications rely on
+ # this within query strings.
+ query_spaces_url = "https://www.python.org:88/doc/?query= "
+ p = urllib.parse.urlsplit(noise.decode("utf-8") + query_spaces_url)
+ self.assertEqual(p.scheme, "https")
+ self.assertEqual(p.netloc, "www.python.org:88")
+ self.assertEqual(p.path, "/doc/")
+ self.assertEqual(p.query, "query= ")
+ self.assertEqual(p.port, 88)
+ self.assertEqual(p.geturl(), query_spaces_url)
+
+ p = urllib.parse.urlsplit("www.pypi.org ")
+ # That "hostname" gets considered a "path" due to the
+ # trailing space and our existing logic... YUCK...
+ # and re-assembles via geturl aka unurlsplit into the original.
+ # django.core.validators.URLValidator (at least through v3.2) relies on
+ # this, for better or worse, to catch it in a ValidationError via its
+ # regular expressions.
+ # Here we test the basic round trip concept of such a trailing space.
+ self.assertEqual(urllib.parse.urlunsplit(p), "www.pypi.org ")
+
+ # with scheme as cache-key
+ url = "//www.python.org/"
+ scheme = noise.decode("utf-8") + "https" + noise.decode("utf-8")
+ for _ in range(2):
+ p = urllib.parse.urlsplit(url, scheme=scheme)
+ self.assertEqual(p.scheme, "https")
+ self.assertEqual(p.geturl(), "https://www.python.org/")
+
def test_attributes_bad_port(self):
"""Check handling of invalid ports."""
for bytes in (False, True):
@@ -667,7 +726,7 @@ class UrlParseTestCase(unittest.TestCase):
for port in ("foo", "1.5", "-1", "0x10"):
with self.subTest(bytes=bytes, parse=parse, port=port):
netloc = "www.example.net:" + port
- url = "http://" + netloc
+ url = "http://" + netloc + "/"
if bytes:
netloc = netloc.encode("ascii")
url = url.encode("ascii")
diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
index ac6e7a9cee..717e990997 100644
--- a/Lib/urllib/parse.py
+++ b/Lib/urllib/parse.py
@@ -25,6 +25,10 @@ currently not entirely compliant with this RFC due to defacto
scenarios for parsing, and for backward compatibility purposes, some
parsing quirks from older RFCs are retained. The testcases in
test_urlparse.py provides a good indicator of parsing behavior.
+
+The WHATWG URL Parser spec should also be considered. We are not compliant with
+it either due to existing user code API behavior expectations (Hyrum's Law).
+It serves as a useful guide when making changes.
"""
import re
@@ -76,6 +80,10 @@ scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
'0123456789'
'+-.')
+# Leading and trailing C0 control and space to be stripped per WHATWG spec.
+# == "".join([chr(i) for i in range(0, 0x20 + 1)])
+_WHATWG_C0_CONTROL_OR_SPACE = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f '
+
# Unsafe bytes to be removed per WHATWG spec
_UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
@@ -426,6 +434,10 @@ def urlsplit(url, scheme='', allow_fragments=True):
url, scheme, _coerce_result = _coerce_args(url, scheme)
url = _remove_unsafe_bytes_from_url(url)
scheme = _remove_unsafe_bytes_from_url(scheme)
+ # Only lstrip url as some applications rely on preserving trailing space.
+ # (https://url.spec.whatwg.org/#concept-basic-url-parser would strip both)
+ url = url.lstrip(_WHATWG_C0_CONTROL_OR_SPACE)
+ scheme = scheme.strip(_WHATWG_C0_CONTROL_OR_SPACE)
allow_fragments = bool(allow_fragments)
key = url, scheme, allow_fragments, type(url), type(scheme)
cached = _parse_cache.get(key, None)
diff --git a/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst b/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
new file mode 100644
index 0000000000..e57ac4ed3a
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2023-03-07-20-59-17.gh-issue-102153.14CLSZ.rst
@@ -0,0 +1,3 @@
+:func:`urllib.parse.urlsplit` now strips leading C0 control and space
+characters following the specification for URLs defined by WHATWG in
+response to CVE-2023-24329. Patch by Illia Volochii.

View File

@ -1,648 +0,0 @@
From 9f39318072b5775cf527f83daf8cb5d64678ac86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
Date: Tue, 22 Aug 2023 19:57:01 +0200
Subject: [PATCH 1/4] gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl
pre-close flaw (#108321)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
gh-108310: Fix CVE-2023-40217: Check for & avoid the ssl pre-close flaw
Instances of `ssl.SSLSocket` were vulnerable to a bypass of the TLS handshake
and included protections (like certificate verification) and treating sent
unencrypted data as if it were post-handshake TLS encrypted data.
The vulnerability is caused when a socket is connected, data is sent by the
malicious peer and stored in a buffer, and then the malicious peer closes the
socket within a small timing window before the other peers TLS handshake can
begin. After this sequence of events the closed socket will not immediately
attempt a TLS handshake due to not being connected but will also allow the
buffered data to be read as if a successful TLS handshake had occurred.
Co-authored-by: Gregory P. Smith [Google LLC] <greg@krypto.org>
---
Lib/ssl.py | 31 ++-
Lib/test/test_ssl.py | 214 ++++++++++++++++++
...-08-22-17-39-12.gh-issue-108310.fVM3sg.rst | 7 +
3 files changed, 251 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
diff --git a/Lib/ssl.py b/Lib/ssl.py
index c5c5529..288f237 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -741,7 +741,7 @@ class SSLSocket(socket):
type=sock.type,
proto=sock.proto,
fileno=sock.fileno())
- self.settimeout(sock.gettimeout())
+ sock_timeout = sock.gettimeout()
sock.detach()
elif fileno is not None:
socket.__init__(self, fileno=fileno)
@@ -755,9 +755,38 @@ class SSLSocket(socket):
if e.errno != errno.ENOTCONN:
raise
connected = False
+ blocking = self.getblocking()
+ self.setblocking(False)
+ try:
+ # We are not connected so this is not supposed to block, but
+ # testing revealed otherwise on macOS and Windows so we do
+ # the non-blocking dance regardless. Our raise when any data
+ # is found means consuming the data is harmless.
+ notconn_pre_handshake_data = self.recv(1)
+ except OSError as e:
+ # EINVAL occurs for recv(1) on non-connected on unix sockets.
+ if e.errno not in (errno.ENOTCONN, errno.EINVAL):
+ raise
+ notconn_pre_handshake_data = b''
+ self.setblocking(blocking)
+ if notconn_pre_handshake_data:
+ # This prevents pending data sent to the socket before it was
+ # closed from escaping to the caller who could otherwise
+ # presume it came through a successful TLS connection.
+ reason = "Closed before TLS handshake with data in recv buffer."
+ notconn_pre_handshake_data_error = SSLError(e.errno, reason)
+ # Add the SSLError attributes that _ssl.c always adds.
+ notconn_pre_handshake_data_error.reason = reason
+ notconn_pre_handshake_data_error.library = None
+ try:
+ self.close()
+ except OSError:
+ pass
+ raise notconn_pre_handshake_data_error
else:
connected = True
+ self.settimeout(sock_timeout) # Must come after setblocking() calls.
self._closed = False
self._sslobj = None
self._connected = connected
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index b35db25..e24d11b 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -3,11 +3,14 @@
import sys
import unittest
from test import support
+import re
import socket
import select
+import struct
import time
import datetime
import gc
+import http.client
import os
import errno
import pprint
@@ -3940,6 +3943,217 @@ class TestPostHandshakeAuth(unittest.TestCase):
# server cert has not been validated
self.assertEqual(s.getpeercert(), {})
+def set_socket_so_linger_on_with_zero_timeout(sock):
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
+
+
+class TestPreHandshakeClose(unittest.TestCase):
+ """Verify behavior of close sockets with received data before to the handshake.
+ """
+
+ class SingleConnectionTestServerThread(threading.Thread):
+
+ def __init__(self, *, name, call_after_accept):
+ self.call_after_accept = call_after_accept
+ self.received_data = b'' # set by .run()
+ self.wrap_error = None # set by .run()
+ self.listener = None # set by .start()
+ self.port = None # set by .start()
+ super().__init__(name=name)
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ def __exit__(self, *args):
+ try:
+ if self.listener:
+ self.listener.close()
+ except OSError:
+ pass
+ self.join()
+ self.wrap_error = None # avoid dangling references
+
+ def start(self):
+ self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED
+ self.ssl_ctx.load_verify_locations(cafile=ONLYCERT)
+ self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
+ self.listener = socket.socket()
+ self.port = support.bind_port(self.listener)
+ self.listener.settimeout(2.0)
+ self.listener.listen(1)
+ super().start()
+
+ def run(self):
+ conn, address = self.listener.accept()
+ self.listener.close()
+ with conn:
+ if self.call_after_accept(conn):
+ return
+ try:
+ tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True)
+ except OSError as err: # ssl.SSLError inherits from OSError
+ self.wrap_error = err
+ else:
+ try:
+ self.received_data = tls_socket.recv(400)
+ except OSError:
+ pass # closed, protocol error, etc.
+
+ def non_linux_skip_if_other_okay_error(self, err):
+ if sys.platform == "linux":
+ return # Expect the full test setup to always work on Linux.
+ if (isinstance(err, ConnectionResetError) or
+ (isinstance(err, OSError) and err.errno == errno.EINVAL) or
+ re.search('wrong.version.number', getattr(err, "reason", ""), re.I)):
+ # On Windows the TCP RST leads to a ConnectionResetError
+ # (ECONNRESET) which Linux doesn't appear to surface to userspace.
+ # If wrap_socket() winds up on the "if connected:" path and doing
+ # the actual wrapping... we get an SSLError from OpenSSL. Typically
+ # WRONG_VERSION_NUMBER. While appropriate, neither is the scenario
+ # we're specifically trying to test. The way this test is written
+ # is known to work on Linux. We'll skip it anywhere else that it
+ # does not present as doing so.
+ self.skipTest("Could not recreate conditions on {}: \
+ err={}".format(sys.platform,err))
+ # If maintaining this conditional winds up being a problem.
+ # just turn this into an unconditional skip anything but Linux.
+ # The important thing is that our CI has the logic covered.
+
+ def test_preauth_data_to_tls_server(self):
+ server_accept_called = threading.Event()
+ ready_for_server_wrap_socket = threading.Event()
+
+ def call_after_accept(unused):
+ server_accept_called.set()
+ if not ready_for_server_wrap_socket.wait(2.0):
+ raise RuntimeError("wrap_socket event never set, test may fail.")
+ return False # Tell the server thread to continue.
+
+ server = self.SingleConnectionTestServerThread(
+ call_after_accept=call_after_accept,
+ name="preauth_data_to_tls_server")
+ server.__enter__() # starts it
+ self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
+
+ with socket.socket() as client:
+ client.connect(server.listener.getsockname())
+ # This forces an immediate connection close via RST on .close().
+ set_socket_so_linger_on_with_zero_timeout(client)
+ client.setblocking(False)
+
+ server_accept_called.wait()
+ client.send(b"DELETE /data HTTP/1.0\r\n\r\n")
+ client.close() # RST
+
+ ready_for_server_wrap_socket.set()
+ server.join()
+ wrap_error = server.wrap_error
+ self.assertEqual(b"", server.received_data)
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
+ self.non_linux_skip_if_other_okay_error(wrap_error)
+ self.assertIsInstance(wrap_error, ssl.SSLError)
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
+ self.assertNotEqual(0, wrap_error.args[0])
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
+
+ def test_preauth_data_to_tls_client(self):
+ client_can_continue_with_wrap_socket = threading.Event()
+
+ def call_after_accept(conn_to_client):
+ # This forces an immediate connection close via RST on .close().
+ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
+ conn_to_client.send(
+ b"HTTP/1.0 307 Temporary Redirect\r\n"
+ b"Location: https://example.com/someone-elses-server\r\n"
+ b"\r\n")
+ conn_to_client.close() # RST
+ client_can_continue_with_wrap_socket.set()
+ return True # Tell the server to stop.
+
+ server = self.SingleConnectionTestServerThread(
+ call_after_accept=call_after_accept,
+ name="preauth_data_to_tls_client")
+ server.__enter__() # starts it
+ self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
+
+ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
+ set_socket_so_linger_on_with_zero_timeout(server.listener)
+
+ with socket.socket() as client:
+ client.connect(server.listener.getsockname())
+ if not client_can_continue_with_wrap_socket.wait(2.0):
+ self.fail("test server took too long.")
+ ssl_ctx = ssl.create_default_context()
+ try:
+ tls_client = ssl_ctx.wrap_socket(
+ client, server_hostname="localhost")
+ except OSError as err: # SSLError inherits from OSError
+ wrap_error = err
+ received_data = b""
+ else:
+ wrap_error = None
+ received_data = tls_client.recv(400)
+ tls_client.close()
+
+ server.join()
+ self.assertEqual(b"", received_data)
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
+ self.non_linux_skip_if_other_okay_error(wrap_error)
+ self.assertIsInstance(wrap_error, ssl.SSLError)
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
+ self.assertNotEqual(0, wrap_error.args[0])
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
+
+ def test_https_client_non_tls_response_ignored(self):
+
+ server_responding = threading.Event()
+
+ class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
+ def connect(self):
+ http.client.HTTPConnection.connect(self)
+ # Wait for our fault injection server to have done its thing.
+ if not server_responding.wait(1.0) and support.verbose:
+ sys.stdout.write("server_responding event never set.")
+ self.sock = self._context.wrap_socket(
+ self.sock, server_hostname=self.host)
+
+ def call_after_accept(conn_to_client):
+ # This forces an immediate connection close via RST on .close().
+ set_socket_so_linger_on_with_zero_timeout(conn_to_client)
+ conn_to_client.send(
+ b"HTTP/1.0 402 Payment Required\r\n"
+ b"\r\n")
+ conn_to_client.close() # RST
+ server_responding.set()
+ return True # Tell the server to stop.
+
+ server = self.SingleConnectionTestServerThread(
+ call_after_accept=call_after_accept,
+ name="non_tls_http_RST_responder")
+ server.__enter__() # starts it
+ self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
+ # Redundant; call_after_accept sets SO_LINGER on the accepted conn.
+ set_socket_so_linger_on_with_zero_timeout(server.listener)
+
+ connection = SynchronizedHTTPSConnection(
+ f"localhost",
+ port=server.port,
+ context=ssl.create_default_context(),
+ timeout=2.0,
+ )
+ # There are lots of reasons this raises as desired, long before this
+ # test was added. Sending the request requires a successful TLS wrapped
+ # socket; that fails if the connection is broken. It may seem pointless
+ # to test this. It serves as an illustration of something that we never
+ # want to happen... properly not happening.
+ with self.assertRaises(OSError) as err_ctx:
+ connection.request("HEAD", "/test", headers={"Host": "localhost"})
+ response = connection.getresponse()
+
def test_main(verbose=False):
if support.verbose:
diff --git a/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst b/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
new file mode 100644
index 0000000..403c77a
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2023-08-22-17-39-12.gh-issue-108310.fVM3sg.rst
@@ -0,0 +1,7 @@
+Fixed an issue where instances of :class:`ssl.SSLSocket` were vulnerable to
+a bypass of the TLS handshake and included protections (like certificate
+verification) and treating sent unencrypted data as if it were
+post-handshake TLS encrypted data. Security issue reported as
+`CVE-2023-40217
+<https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-40217>`_ by
+Aapo Oksman. Patch by Gregory P. Smith.
--
2.41.0
From 6fbb37c0b7ab8ce1db8e2e78df62d6e5c1e56766 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
<31488909+miss-islington@users.noreply.github.com>
Date: Wed, 23 Aug 2023 03:10:56 -0700
Subject: [PATCH 2/4] gh-108342: Break ref cycle in SSLSocket._create() exc
(GH-108344) (#108352)
Explicitly break a reference cycle when SSLSocket._create() raises an
exception. Clear the variable storing the exception, since the
exception traceback contains the variables and so creates a reference
cycle.
This test leak was introduced by the test added for the fix of GH-108310.
(cherry picked from commit 64f99350351bc46e016b2286f36ba7cd669b79e3)
Co-authored-by: Victor Stinner <vstinner@python.org>
---
Lib/ssl.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/Lib/ssl.py b/Lib/ssl.py
index 288f237..67869c9 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -782,7 +782,11 @@ class SSLSocket(socket):
self.close()
except OSError:
pass
- raise notconn_pre_handshake_data_error
+ try:
+ raise notconn_pre_handshake_data_error
+ finally:
+ # Explicitly break the reference cycle.
+ notconn_pre_handshake_data_error = None
else:
connected = True
--
2.41.0
From 579d809e60e569f06c00844cc3a3f06a0e359603 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
Date: Thu, 24 Aug 2023 12:09:30 +0200
Subject: [PATCH 3/4] gh-108342: Make ssl TestPreHandshakeClose more reliable
(GH-108370) (#108408)
* In preauth tests of test_ssl, explicitly break reference cycles
invoving SingleConnectionTestServerThread to make sure that the
thread is deleted. Otherwise, the test marks the environment as
altered because the threading module sees a "dangling thread"
(SingleConnectionTestServerThread). This test leak was introduced
by the test added for the fix of issue gh-108310.
* Use support.SHORT_TIMEOUT instead of hardcoded 1.0 or 2.0 seconds
timeout.
* SingleConnectionTestServerThread.run() catchs TimeoutError
* Fix a race condition (missing synchronization) in
test_preauth_data_to_tls_client(): the server now waits until the
client connect() completed in call_after_accept().
* test_https_client_non_tls_response_ignored() calls server.join()
explicitly.
* Replace "localhost" with server.listener.getsockname()[0].
(cherry picked from commit 592bacb6fc0833336c0453e818e9b95016e9fd47)
---
Lib/test/test_ssl.py | 102 ++++++++++++++++++++++++++++++-------------
1 file changed, 71 insertions(+), 31 deletions(-)
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index e24d11b..6332330 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -3953,12 +3953,16 @@ class TestPreHandshakeClose(unittest.TestCase):
class SingleConnectionTestServerThread(threading.Thread):
- def __init__(self, *, name, call_after_accept):
+ def __init__(self, *, name, call_after_accept, timeout=None):
self.call_after_accept = call_after_accept
self.received_data = b'' # set by .run()
self.wrap_error = None # set by .run()
self.listener = None # set by .start()
self.port = None # set by .start()
+ if timeout is None:
+ self.timeout = support.SHORT_TIMEOUT
+ else:
+ self.timeout = timeout
super().__init__(name=name)
def __enter__(self):
@@ -3981,13 +3985,19 @@ class TestPreHandshakeClose(unittest.TestCase):
self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY)
self.listener = socket.socket()
self.port = support.bind_port(self.listener)
- self.listener.settimeout(2.0)
+ self.listener.settimeout(self.timeout)
self.listener.listen(1)
super().start()
def run(self):
- conn, address = self.listener.accept()
- self.listener.close()
+ try:
+ conn, address = self.listener.accept()
+ except TimeoutError:
+ # on timeout, just close the listener
+ return
+ finally:
+ self.listener.close()
+
with conn:
if self.call_after_accept(conn):
return
@@ -4015,8 +4025,13 @@ class TestPreHandshakeClose(unittest.TestCase):
# we're specifically trying to test. The way this test is written
# is known to work on Linux. We'll skip it anywhere else that it
# does not present as doing so.
- self.skipTest("Could not recreate conditions on {}: \
- err={}".format(sys.platform,err))
+ try:
+ self.skipTest("Could not recreate conditions on {}: \
+ err={}".format(sys.platform,err))
+ finally:
+ # gh-108342: Explicitly break the reference cycle
+ err = None
+
# If maintaining this conditional winds up being a problem.
# just turn this into an unconditional skip anything but Linux.
# The important thing is that our CI has the logic covered.
@@ -4027,7 +4042,7 @@ class TestPreHandshakeClose(unittest.TestCase):
def call_after_accept(unused):
server_accept_called.set()
- if not ready_for_server_wrap_socket.wait(2.0):
+ if not ready_for_server_wrap_socket.wait(support.SHORT_TIMEOUT):
raise RuntimeError("wrap_socket event never set, test may fail.")
return False # Tell the server thread to continue.
@@ -4049,20 +4064,31 @@ class TestPreHandshakeClose(unittest.TestCase):
ready_for_server_wrap_socket.set()
server.join()
+
wrap_error = server.wrap_error
- self.assertEqual(b"", server.received_data)
- self.assertIsInstance(wrap_error, OSError) # All platforms.
- self.non_linux_skip_if_other_okay_error(wrap_error)
- self.assertIsInstance(wrap_error, ssl.SSLError)
- self.assertIn("before TLS handshake with data", wrap_error.args[1])
- self.assertIn("before TLS handshake with data", wrap_error.reason)
- self.assertNotEqual(0, wrap_error.args[0])
- self.assertIsNone(wrap_error.library, msg="attr must exist")
+ server.wrap_error = None
+ try:
+ self.assertEqual(b"", server.received_data)
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
+ self.non_linux_skip_if_other_okay_error(wrap_error)
+ self.assertIsInstance(wrap_error, ssl.SSLError)
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
+ self.assertNotEqual(0, wrap_error.args[0])
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
+ finally:
+ # gh-108342: Explicitly break the reference cycle
+ wrap_error = None
+ server = None
def test_preauth_data_to_tls_client(self):
+ server_can_continue_with_wrap_socket = threading.Event()
client_can_continue_with_wrap_socket = threading.Event()
def call_after_accept(conn_to_client):
+ if not server_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
+ print("ERROR: test client took too long")
+
# This forces an immediate connection close via RST on .close().
set_socket_so_linger_on_with_zero_timeout(conn_to_client)
conn_to_client.send(
@@ -4084,8 +4110,10 @@ class TestPreHandshakeClose(unittest.TestCase):
with socket.socket() as client:
client.connect(server.listener.getsockname())
- if not client_can_continue_with_wrap_socket.wait(2.0):
- self.fail("test server took too long.")
+ server_can_continue_with_wrap_socket.set()
+
+ if not client_can_continue_with_wrap_socket.wait(support.SHORT_TIMEOUT):
+ self.fail("test server took too long")
ssl_ctx = ssl.create_default_context()
try:
tls_client = ssl_ctx.wrap_socket(
@@ -4099,24 +4127,31 @@ class TestPreHandshakeClose(unittest.TestCase):
tls_client.close()
server.join()
- self.assertEqual(b"", received_data)
- self.assertIsInstance(wrap_error, OSError) # All platforms.
- self.non_linux_skip_if_other_okay_error(wrap_error)
- self.assertIsInstance(wrap_error, ssl.SSLError)
- self.assertIn("before TLS handshake with data", wrap_error.args[1])
- self.assertIn("before TLS handshake with data", wrap_error.reason)
- self.assertNotEqual(0, wrap_error.args[0])
- self.assertIsNone(wrap_error.library, msg="attr must exist")
+ try:
+ self.assertEqual(b"", received_data)
+ self.assertIsInstance(wrap_error, OSError) # All platforms.
+ self.non_linux_skip_if_other_okay_error(wrap_error)
+ self.assertIsInstance(wrap_error, ssl.SSLError)
+ self.assertIn("before TLS handshake with data", wrap_error.args[1])
+ self.assertIn("before TLS handshake with data", wrap_error.reason)
+ self.assertNotEqual(0, wrap_error.args[0])
+ self.assertIsNone(wrap_error.library, msg="attr must exist")
+ finally:
+ # gh-108342: Explicitly break the reference cycle
+ wrap_error = None
+ server = None
def test_https_client_non_tls_response_ignored(self):
-
server_responding = threading.Event()
class SynchronizedHTTPSConnection(http.client.HTTPSConnection):
def connect(self):
+ # Call clear text HTTP connect(), not the encrypted HTTPS (TLS)
+ # connect(): wrap_socket() is called manually below.
http.client.HTTPConnection.connect(self)
+
# Wait for our fault injection server to have done its thing.
- if not server_responding.wait(1.0) and support.verbose:
+ if not server_responding.wait(support.SHORT_TIMEOUT) and support.verbose:
sys.stdout.write("server_responding event never set.")
self.sock = self._context.wrap_socket(
self.sock, server_hostname=self.host)
@@ -4131,29 +4166,34 @@ class TestPreHandshakeClose(unittest.TestCase):
server_responding.set()
return True # Tell the server to stop.
+ timeout = 2.0
server = self.SingleConnectionTestServerThread(
call_after_accept=call_after_accept,
- name="non_tls_http_RST_responder")
+ name="non_tls_http_RST_responder",
+ timeout=timeout)
server.__enter__() # starts it
self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it.
# Redundant; call_after_accept sets SO_LINGER on the accepted conn.
set_socket_so_linger_on_with_zero_timeout(server.listener)
connection = SynchronizedHTTPSConnection(
- f"localhost",
+ server.listener.getsockname()[0],
port=server.port,
context=ssl.create_default_context(),
- timeout=2.0,
+ timeout=timeout,
)
+
# There are lots of reasons this raises as desired, long before this
# test was added. Sending the request requires a successful TLS wrapped
# socket; that fails if the connection is broken. It may seem pointless
# to test this. It serves as an illustration of something that we never
# want to happen... properly not happening.
- with self.assertRaises(OSError) as err_ctx:
+ with self.assertRaises(OSError):
connection.request("HEAD", "/test", headers={"Host": "localhost"})
response = connection.getresponse()
+ server.join()
+
def test_main(verbose=False):
if support.verbose:
--
2.41.0
From 2e8776245e9fb8ede784077df26684b4b53df0bc Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Mon, 25 Sep 2023 21:55:29 +0200
Subject: [PATCH 4/4] Downstream: Additional fixup for 3.6:
Use alternative for self.getblocking(), which was added in Python 3.7
see: https://docs.python.org/3/library/socket.html#socket.socket.getblocking
Set self._sslobj early to avoid AttributeError
---
Lib/ssl.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Lib/ssl.py b/Lib/ssl.py
index 67869c9..daedc82 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -690,6 +690,7 @@ class SSLSocket(socket):
suppress_ragged_eofs=True, npn_protocols=None, ciphers=None,
server_hostname=None,
_context=None, _session=None):
+ self._sslobj = None
if _context:
self._context = _context
@@ -755,7 +756,7 @@ class SSLSocket(socket):
if e.errno != errno.ENOTCONN:
raise
connected = False
- blocking = self.getblocking()
+ blocking = (self.gettimeout() != 0)
self.setblocking(False)
try:
# We are not connected so this is not supposed to block, but
--
2.41.0

View File

@ -1,143 +0,0 @@
From c563f409ea30bcb0623d785428c9257917371b76 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
<31488909+miss-islington@users.noreply.github.com>
Date: Thu, 23 Jan 2020 06:49:19 -0800
Subject: [PATCH] bpo-39421: Fix posible crash in heapq with custom comparison
operators (GH-18118) (GH-18146)
(cherry picked from commit 79f89e6e5a659846d1068e8b1bd8e491ccdef861)
Co-authored-by: Pablo Galindo <Pablogsal@gmail.com>
---
Lib/test/test_heapq.py | 31 ++++++++++++++++
.../2020-01-22-15-53-37.bpo-39421.O3nG7u.rst | 2 ++
Modules/_heapqmodule.c | 35 ++++++++++++++-----
3 files changed, 59 insertions(+), 9 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-01-22-15-53-37.bpo-39421.O3nG7u.rst
diff --git a/Lib/test/test_heapq.py b/Lib/test/test_heapq.py
index 2f8c648d84a58..7c3fb0210f69b 100644
--- a/Lib/test/test_heapq.py
+++ b/Lib/test/test_heapq.py
@@ -414,6 +414,37 @@ def test_heappop_mutating_heap(self):
with self.assertRaises((IndexError, RuntimeError)):
self.module.heappop(heap)
+ def test_comparison_operator_modifiying_heap(self):
+ # See bpo-39421: Strong references need to be taken
+ # when comparing objects as they can alter the heap
+ class EvilClass(int):
+ def __lt__(self, o):
+ heap.clear()
+ return NotImplemented
+
+ heap = []
+ self.module.heappush(heap, EvilClass(0))
+ self.assertRaises(IndexError, self.module.heappushpop, heap, 1)
+
+ def test_comparison_operator_modifiying_heap_two_heaps(self):
+
+ class h(int):
+ def __lt__(self, o):
+ list2.clear()
+ return NotImplemented
+
+ class g(int):
+ def __lt__(self, o):
+ list1.clear()
+ return NotImplemented
+
+ list1, list2 = [], []
+
+ self.module.heappush(list1, h(0))
+ self.module.heappush(list2, g(0))
+
+ self.assertRaises((IndexError, RuntimeError), self.module.heappush, list1, g(1))
+ self.assertRaises((IndexError, RuntimeError), self.module.heappush, list2, h(1))
class TestErrorHandlingPython(TestErrorHandling, TestCase):
module = py_heapq
diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-01-22-15-53-37.bpo-39421.O3nG7u.rst b/Misc/NEWS.d/next/Core and Builtins/2020-01-22-15-53-37.bpo-39421.O3nG7u.rst
new file mode 100644
index 0000000000000..bae008150ee12
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2020-01-22-15-53-37.bpo-39421.O3nG7u.rst
@@ -0,0 +1,2 @@
+Fix possible crashes when operating with the functions in the :mod:`heapq`
+module and custom comparison operators.
diff --git a/Modules/_heapqmodule.c b/Modules/_heapqmodule.c
index b499e1f668aae..0fb35ffe5ec48 100644
--- a/Modules/_heapqmodule.c
+++ b/Modules/_heapqmodule.c
@@ -29,7 +29,11 @@ siftdown(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
while (pos > startpos) {
parentpos = (pos - 1) >> 1;
parent = arr[parentpos];
+ Py_INCREF(newitem);
+ Py_INCREF(parent);
cmp = PyObject_RichCompareBool(newitem, parent, Py_LT);
+ Py_DECREF(parent);
+ Py_DECREF(newitem);
if (cmp < 0)
return -1;
if (size != PyList_GET_SIZE(heap)) {
@@ -71,10 +75,13 @@ siftup(PyListObject *heap, Py_ssize_t pos)
/* Set childpos to index of smaller child. */
childpos = 2*pos + 1; /* leftmost child position */
if (childpos + 1 < endpos) {
- cmp = PyObject_RichCompareBool(
- arr[childpos],
- arr[childpos + 1],
- Py_LT);
+ PyObject* a = arr[childpos];
+ PyObject* b = arr[childpos + 1];
+ Py_INCREF(a);
+ Py_INCREF(b);
+ cmp = PyObject_RichCompareBool(a, b, Py_LT);
+ Py_DECREF(a);
+ Py_DECREF(b);
if (cmp < 0)
return -1;
childpos += ((unsigned)cmp ^ 1); /* increment when cmp==0 */
@@ -229,7 +236,10 @@ heappushpop(PyObject *self, PyObject *args)
return item;
}
- cmp = PyObject_RichCompareBool(PyList_GET_ITEM(heap, 0), item, Py_LT);
+ PyObject* top = PyList_GET_ITEM(heap, 0);
+ Py_INCREF(top);
+ cmp = PyObject_RichCompareBool(top, item, Py_LT);
+ Py_DECREF(top);
if (cmp < 0)
return NULL;
if (cmp == 0) {
@@ -383,7 +393,11 @@ siftdown_max(PyListObject *heap, Py_ssize_t startpos, Py_ssize_t pos)
while (pos > startpos) {
parentpos = (pos - 1) >> 1;
parent = arr[parentpos];
+ Py_INCREF(parent);
+ Py_INCREF(newitem);
cmp = PyObject_RichCompareBool(parent, newitem, Py_LT);
+ Py_DECREF(parent);
+ Py_DECREF(newitem);
if (cmp < 0)
return -1;
if (size != PyList_GET_SIZE(heap)) {
@@ -425,10 +439,13 @@ siftup_max(PyListObject *heap, Py_ssize_t pos)
/* Set childpos to index of smaller child. */
childpos = 2*pos + 1; /* leftmost child position */
if (childpos + 1 < endpos) {
- cmp = PyObject_RichCompareBool(
- arr[childpos + 1],
- arr[childpos],
- Py_LT);
+ PyObject* a = arr[childpos + 1];
+ PyObject* b = arr[childpos];
+ Py_INCREF(a);
+ Py_INCREF(b);
+ cmp = PyObject_RichCompareBool(a, b, Py_LT);
+ Py_DECREF(a);
+ Py_DECREF(b);
if (cmp < 0)
return -1;
childpos += ((unsigned)cmp ^ 1); /* increment when cmp==0 */

View File

@ -1,560 +0,0 @@
From cec66eefbee76ee95f252a52de0f5abc1b677f5b Mon Sep 17 00:00:00 2001
From: Lumir Balhar <lbalhar@redhat.com>
Date: Tue, 12 Dec 2023 13:03:42 +0100
Subject: [PATCH] [3.6] bpo-42103: Improve validation of Plist files.
(GH-22882) (GH-23118)
* Prevent some possible DoS attacks via providing invalid Plist files
with extremely large number of objects or collection sizes.
* Raise InvalidFileException for too large bytes and string size instead of returning garbage.
* Raise InvalidFileException instead of ValueError for specific invalid datetime (NaN).
* Raise InvalidFileException instead of TypeError for non-hashable dict keys.
* Add more tests for invalid Plist files..
(cherry picked from commit 34637a0ce21e7261b952fbd9d006474cc29b681f)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Lib/plistlib.py | 34 +-
Lib/test/test_plistlib.py | 394 +++++++++++++++---
.../2020-10-23-19-20-14.bpo-42103.C5obK2.rst | 3 +
.../2020-10-23-19-19-30.bpo-42103.cILT66.rst | 2 +
4 files changed, 366 insertions(+), 67 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2020-10-23-19-20-14.bpo-42103.C5obK2.rst
create mode 100644 Misc/NEWS.d/next/Security/2020-10-23-19-19-30.bpo-42103.cILT66.rst
diff --git a/Lib/plistlib.py b/Lib/plistlib.py
index a918643..df1f346 100644
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -626,7 +626,7 @@ class _BinaryPlistParser:
return self._read_object(top_object)
except (OSError, IndexError, struct.error, OverflowError,
- UnicodeDecodeError):
+ ValueError):
raise InvalidFileException()
def _get_size(self, tokenL):
@@ -642,7 +642,7 @@ class _BinaryPlistParser:
def _read_ints(self, n, size):
data = self._fp.read(size * n)
if size in _BINARY_FORMAT:
- return struct.unpack('>' + _BINARY_FORMAT[size] * n, data)
+ return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
else:
if not size or len(data) != size * n:
raise InvalidFileException()
@@ -701,19 +701,25 @@ class _BinaryPlistParser:
elif tokenH == 0x40: # data
s = self._get_size(tokenL)
- if self._use_builtin_types:
- result = self._fp.read(s)
- else:
- result = Data(self._fp.read(s))
+ result = self._fp.read(s)
+ if len(result) != s:
+ raise InvalidFileException()
+ if not self._use_builtin_types:
+ result = Data(result)
elif tokenH == 0x50: # ascii string
s = self._get_size(tokenL)
- result = self._fp.read(s).decode('ascii')
- result = result
+ data = self._fp.read(s)
+ if len(data) != s:
+ raise InvalidFileException()
+ result = data.decode('ascii')
elif tokenH == 0x60: # unicode string
- s = self._get_size(tokenL)
- result = self._fp.read(s * 2).decode('utf-16be')
+ s = self._get_size(tokenL) * 2
+ data = self._fp.read(s)
+ if len(data) != s:
+ raise InvalidFileException()
+ result = data.decode('utf-16be')
# tokenH == 0x80 is documented as 'UID' and appears to be used for
# keyed-archiving, not in plists.
@@ -737,9 +743,11 @@ class _BinaryPlistParser:
obj_refs = self._read_refs(s)
result = self._dict_type()
self._objects[ref] = result
- for k, o in zip(key_refs, obj_refs):
- result[self._read_object(k)] = self._read_object(o)
-
+ try:
+ for k, o in zip(key_refs, obj_refs):
+ result[self._read_object(k)] = self._read_object(o)
+ except TypeError:
+ raise InvalidFileException()
else:
raise InvalidFileException()
diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
index d47c607..f71245d 100644
--- a/Lib/test/test_plistlib.py
+++ b/Lib/test/test_plistlib.py
@@ -1,5 +1,6 @@
# Copyright (C) 2003-2013 Python Software Foundation
+import struct
import unittest
import plistlib
import os
@@ -90,6 +91,284 @@ TESTDATA={
xQHHAsQC0gAAAAAAAAIBAAAAAAAAADkAAAAAAAAAAAAAAAAAAALs'''),
}
+INVALID_BINARY_PLISTS = [
+ ('too short data',
+ b''
+ ),
+ ('too large offset_table_offset and offset_size = 1',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x2a'
+ ),
+ ('too large offset_table_offset and nonstandard offset_size',
+ b'\x00\x00\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x03\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x2c'
+ ),
+ ('integer overflow in offset_table_offset',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\xff\xff\xff\xff\xff\xff\xff\xff'
+ ),
+ ('too large top_object',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('integer overflow in top_object',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\xff\xff\xff\xff\xff\xff\xff\xff'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('too large num_objects and offset_size = 1',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\xff'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('too large num_objects and nonstandard offset_size',
+ b'\x00\x00\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x03\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\xff'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('extremally large num_objects (32 bit)',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x7f\xff\xff\xff'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('extremally large num_objects (64 bit)',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\xff\xff\xff\xff\xff'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('integer overflow in num_objects',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\xff\xff\xff\xff\xff\xff\xff\xff'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('offset_size = 0',
+ b'\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('ref_size = 0',
+ b'\xa1\x01\x00\x08\x0a'
+ b'\x00\x00\x00\x00\x00\x00\x01\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0b'
+ ),
+ ('too large offset',
+ b'\x00\x2a'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('integer overflow in offset',
+ b'\x00\xff\xff\xff\xff\xff\xff\xff\xff'
+ b'\x00\x00\x00\x00\x00\x00\x08\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x09'
+ ),
+ ('too large array size',
+ b'\xaf\x00\x01\xff\x00\x08\x0c'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0d'
+ ),
+ ('extremally large array size (32-bit)',
+ b'\xaf\x02\x7f\xff\xff\xff\x01\x00\x08\x0f'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x10'
+ ),
+ ('extremally large array size (64-bit)',
+ b'\xaf\x03\x00\x00\x00\xff\xff\xff\xff\xff\x01\x00\x08\x13'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x14'
+ ),
+ ('integer overflow in array size',
+ b'\xaf\x03\xff\xff\xff\xff\xff\xff\xff\xff\x01\x00\x08\x13'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x14'
+ ),
+ ('too large reference index',
+ b'\xa1\x02\x00\x08\x0a'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0b'
+ ),
+ ('integer overflow in reference index',
+ b'\xa1\xff\xff\xff\xff\xff\xff\xff\xff\x00\x08\x11'
+ b'\x00\x00\x00\x00\x00\x00\x01\x08'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x12'
+ ),
+ ('too large bytes size',
+ b'\x4f\x00\x23\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0c'
+ ),
+ ('extremally large bytes size (32-bit)',
+ b'\x4f\x02\x7f\xff\xff\xff\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0f'
+ ),
+ ('extremally large bytes size (64-bit)',
+ b'\x4f\x03\x00\x00\x00\xff\xff\xff\xff\xff\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x13'
+ ),
+ ('integer overflow in bytes size',
+ b'\x4f\x03\xff\xff\xff\xff\xff\xff\xff\xff\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x13'
+ ),
+ ('too large ASCII size',
+ b'\x5f\x00\x23\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0c'
+ ),
+ ('extremally large ASCII size (32-bit)',
+ b'\x5f\x02\x7f\xff\xff\xff\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0f'
+ ),
+ ('extremally large ASCII size (64-bit)',
+ b'\x5f\x03\x00\x00\x00\xff\xff\xff\xff\xff\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x13'
+ ),
+ ('integer overflow in ASCII size',
+ b'\x5f\x03\xff\xff\xff\xff\xff\xff\xff\xff\x41\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x13'
+ ),
+ ('invalid ASCII',
+ b'\x51\xff\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0a'
+ ),
+ ('too large UTF-16 size',
+ b'\x6f\x00\x13\x20\xac\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0e'
+ ),
+ ('extremally large UTF-16 size (32-bit)',
+ b'\x6f\x02\x4f\xff\xff\xff\x20\xac\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x11'
+ ),
+ ('extremally large UTF-16 size (64-bit)',
+ b'\x6f\x03\x00\x00\x00\xff\xff\xff\xff\xff\x20\xac\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x15'
+ ),
+ ('integer overflow in UTF-16 size',
+ b'\x6f\x03\xff\xff\xff\xff\xff\xff\xff\xff\x20\xac\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x15'
+ ),
+ ('invalid UTF-16',
+ b'\x61\xd8\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0b'
+ ),
+ ('non-hashable key',
+ b'\xd1\x01\x01\xa0\x08\x0b'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x02'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x0c'
+ ),
+ ('too large datetime (datetime overflow)',
+ b'\x33\x42\x50\x00\x00\x00\x00\x00\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x11'
+ ),
+ ('too large datetime (timedelta overflow)',
+ b'\x33\x42\xe0\x00\x00\x00\x00\x00\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x11'
+ ),
+ ('invalid datetime (Infinity)',
+ b'\x33\x7f\xf0\x00\x00\x00\x00\x00\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x11'
+ ),
+ ('invalid datetime (NaN)',
+ b'\x33\x7f\xf8\x00\x00\x00\x00\x00\x00\x08'
+ b'\x00\x00\x00\x00\x00\x00\x01\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x01'
+ b'\x00\x00\x00\x00\x00\x00\x00\x00'
+ b'\x00\x00\x00\x00\x00\x00\x00\x11'
+ ),
+]
class TestPlistlib(unittest.TestCase):
@@ -447,6 +726,21 @@ class TestPlistlib(unittest.TestCase):
class TestBinaryPlistlib(unittest.TestCase):
+ @staticmethod
+ def decode(*objects, offset_size=1, ref_size=1):
+ data = [b'bplist00']
+ offset = 8
+ offsets = []
+ for x in objects:
+ offsets.append(offset.to_bytes(offset_size, 'big'))
+ data.append(x)
+ offset += len(x)
+ tail = struct.pack('>6xBBQQQ', offset_size, ref_size,
+ len(objects), 0, offset)
+ data.extend(offsets)
+ data.append(tail)
+ return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
+
def test_nonstandard_refs_size(self):
# Issue #21538: Refs and offsets are 24-bit integers
data = (b'bplist00'
@@ -461,7 +755,7 @@ class TestBinaryPlistlib(unittest.TestCase):
def test_dump_duplicates(self):
# Test effectiveness of saving duplicated objects
- for x in (None, False, True, 12345, 123.45, 'abcde', b'abcde',
+ for x in (None, False, True, 12345, 123.45, 'abcde', 'абвгд', b'abcde',
datetime.datetime(2004, 10, 26, 10, 33, 33),
plistlib.Data(b'abcde'), bytearray(b'abcde'),
[12, 345], (12, 345), {'12': 345}):
@@ -500,6 +794,20 @@ class TestBinaryPlistlib(unittest.TestCase):
b = plistlib.loads(plistlib.dumps(a, fmt=plistlib.FMT_BINARY))
self.assertIs(b['x'], b)
+ def test_deep_nesting(self):
+ for N in [300, 100000]:
+ chunks = [b'\xa1' + (i + 1).to_bytes(4, 'big') for i in range(N)]
+ try:
+ result = self.decode(*chunks, b'\x54seed', offset_size=4, ref_size=4)
+ except RecursionError:
+ pass
+ else:
+ for i in range(N):
+ self.assertIsInstance(result, list)
+ self.assertEqual(len(result), 1)
+ result = result[0]
+ self.assertEqual(result, 'seed')
+
def test_large_timestamp(self):
# Issue #26709: 32-bit timestamp out of range
for ts in -2**31-1, 2**31:
@@ -509,55 +817,37 @@ class TestBinaryPlistlib(unittest.TestCase):
data = plistlib.dumps(d, fmt=plistlib.FMT_BINARY)
self.assertEqual(plistlib.loads(data), d)
+ def test_load_singletons(self):
+ self.assertIs(self.decode(b'\x00'), None)
+ self.assertIs(self.decode(b'\x08'), False)
+ self.assertIs(self.decode(b'\x09'), True)
+ self.assertEqual(self.decode(b'\x0f'), b'')
+
+ def test_load_int(self):
+ self.assertEqual(self.decode(b'\x10\x00'), 0)
+ self.assertEqual(self.decode(b'\x10\xfe'), 0xfe)
+ self.assertEqual(self.decode(b'\x11\xfe\xdc'), 0xfedc)
+ self.assertEqual(self.decode(b'\x12\xfe\xdc\xba\x98'), 0xfedcba98)
+ self.assertEqual(self.decode(b'\x13\x01\x23\x45\x67\x89\xab\xcd\xef'),
+ 0x0123456789abcdef)
+ self.assertEqual(self.decode(b'\x13\xfe\xdc\xba\x98\x76\x54\x32\x10'),
+ -0x123456789abcdf0)
+
+ def test_unsupported(self):
+ unsupported = [*range(1, 8), *range(10, 15),
+ 0x20, 0x21, *range(0x24, 0x33), *range(0x34, 0x40)]
+ for i in [0x70, 0x90, 0xb0, 0xc0, 0xe0, 0xf0]:
+ unsupported.extend(i + j for j in range(16))
+ for token in unsupported:
+ with self.subTest(f'token {token:02x}'):
+ with self.assertRaises(plistlib.InvalidFileException):
+ self.decode(bytes([token]) + b'\x00'*16)
+
def test_invalid_binary(self):
- for data in [
- # too short data
- b'',
- # too large offset_table_offset and nonstandard offset_size
- b'\x00\x08'
- b'\x00\x00\x00\x00\x00\x00\x03\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x2a',
- # integer overflow in offset_table_offset
- b'\x00\x08'
- b'\x00\x00\x00\x00\x00\x00\x01\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\xff\xff\xff\xff\xff\xff\xff\xff',
- # offset_size = 0
- b'\x00\x08'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x09',
- # ref_size = 0
- b'\xa1\x01\x00\x08\x0a'
- b'\x00\x00\x00\x00\x00\x00\x01\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x02'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x0b',
- # integer overflow in offset
- b'\x00\xff\xff\xff\xff\xff\xff\xff\xff'
- b'\x00\x00\x00\x00\x00\x00\x08\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x09',
- # invalid ASCII
- b'\x51\xff\x08'
- b'\x00\x00\x00\x00\x00\x00\x01\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x0a',
- # invalid UTF-16
- b'\x61\xd8\x00\x08'
- b'\x00\x00\x00\x00\x00\x00\x01\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x01'
- b'\x00\x00\x00\x00\x00\x00\x00\x00'
- b'\x00\x00\x00\x00\x00\x00\x00\x0b',
- ]:
- with self.assertRaises(plistlib.InvalidFileException):
- plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
+ for name, data in INVALID_BINARY_PLISTS:
+ with self.subTest(name):
+ with self.assertRaises(plistlib.InvalidFileException):
+ plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
class TestPlistlibDeprecated(unittest.TestCase):
@@ -655,9 +945,5 @@ class MiscTestCase(unittest.TestCase):
support.check__all__(self, plistlib, blacklist=blacklist)
-def test_main():
- support.run_unittest(TestPlistlib, TestPlistlibDeprecated, MiscTestCase)
-
-
if __name__ == '__main__':
- test_main()
+ unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2020-10-23-19-20-14.bpo-42103.C5obK2.rst b/Misc/NEWS.d/next/Library/2020-10-23-19-20-14.bpo-42103.C5obK2.rst
new file mode 100644
index 0000000..4eb694c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-10-23-19-20-14.bpo-42103.C5obK2.rst
@@ -0,0 +1,3 @@
+:exc:`~plistlib.InvalidFileException` and :exc:`RecursionError` are now
+the only errors caused by loading malformed binary Plist file (previously
+ValueError and TypeError could be raised in some specific cases).
diff --git a/Misc/NEWS.d/next/Security/2020-10-23-19-19-30.bpo-42103.cILT66.rst b/Misc/NEWS.d/next/Security/2020-10-23-19-19-30.bpo-42103.cILT66.rst
new file mode 100644
index 0000000..15d7b65
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2020-10-23-19-19-30.bpo-42103.cILT66.rst
@@ -0,0 +1,2 @@
+Prevented potential DoS attack via CPU and RAM exhaustion when processing
+malformed Apple Property List files in binary format.
--
2.43.0

View File

@ -1,88 +0,0 @@
From 0d02ff99721f7650e39ba4c7d8fe06f412bbb591 Mon Sep 17 00:00:00 2001
From: Victor Stinner <vstinner@python.org>
Date: Wed, 13 Dec 2023 11:50:26 +0100
Subject: [PATCH] bpo-46623: Skip two test_zlib tests on s390x (GH-31096)
Skip test_pair() and test_speech128() of test_zlib on s390x since
they fail if zlib uses the s390x hardware accelerator.
---
Lib/test/test_zlib.py | 32 +++++++++++++++++++
.../2022-02-03-09-45-26.bpo-46623.vxzuhV.rst | 2 ++
2 files changed, 34 insertions(+)
create mode 100644 Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst
diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py
index b7170b4..770a425 100644
--- a/Lib/test/test_zlib.py
+++ b/Lib/test/test_zlib.py
@@ -1,6 +1,7 @@
import unittest
from test import support
import binascii
+import os
import pickle
import random
import sys
@@ -15,6 +16,35 @@ requires_Decompress_copy = unittest.skipUnless(
hasattr(zlib.decompressobj(), "copy"),
'requires Decompress.copy()')
+# bpo-46623: On s390x, when a hardware accelerator is used, using different
+# ways to compress data with zlib can produce different compressed data.
+# Simplified test_pair() code:
+#
+# def func1(data):
+# return zlib.compress(data)
+#
+# def func2(data)
+# co = zlib.compressobj()
+# x1 = co.compress(data)
+# x2 = co.flush()
+# return x1 + x2
+#
+# On s390x if zlib uses a hardware accelerator, func1() creates a single
+# "final" compressed block whereas func2() produces 3 compressed blocks (the
+# last one is a final block). On other platforms with no accelerator, func1()
+# and func2() produce the same compressed data made of a single (final)
+# compressed block.
+#
+# Only the compressed data is different, the decompression returns the original
+# data:
+#
+# zlib.decompress(func1(data)) == zlib.decompress(func2(data)) == data
+#
+# Make the assumption that s390x always has an accelerator to simplify the skip
+# condition. Windows doesn't have os.uname() but it doesn't support s390x.
+skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',
+ 'skipped on s390x')
+
class VersionTestCase(unittest.TestCase):
@@ -174,6 +204,7 @@ class CompressTestCase(BaseCompressTestCase, unittest.TestCase):
bufsize=zlib.DEF_BUF_SIZE),
HAMLET_SCENE)
+ @skip_on_s390x
def test_speech128(self):
# compress more data
data = HAMLET_SCENE * 128
@@ -225,6 +256,7 @@ class CompressTestCase(BaseCompressTestCase, unittest.TestCase):
class CompressObjectTestCase(BaseCompressTestCase, unittest.TestCase):
# Test compression object
+ @skip_on_s390x
def test_pair(self):
# straightforward compress/decompress objects
datasrc = HAMLET_SCENE * 128
diff --git a/Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst b/Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst
new file mode 100644
index 0000000..be085c0
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2022-02-03-09-45-26.bpo-46623.vxzuhV.rst
@@ -0,0 +1,2 @@
+Skip test_pair() and test_speech128() of test_zlib on s390x since they fail
+if zlib uses the s390x hardware accelerator. Patch by Victor Stinner.
--
2.43.0

View File

@ -1,750 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Victor Stinner <vstinner@python.org>
Date: Fri, 15 Dec 2023 16:10:40 +0100
Subject: [PATCH] 00415: [CVE-2023-27043] gh-102988: Reject malformed addresses
in email.parseaddr() (#111116)
Detect email address parsing errors and return empty tuple to
indicate the parsing error (old API). Add an optional 'strict'
parameter to getaddresses() and parseaddr() functions. Patch by
Thomas Dwyer.
Co-Authored-By: Thomas Dwyer <github@tomd.tel>
---
Doc/library/email.utils.rst | 19 +-
Lib/email/utils.py | 151 ++++++++++++-
Lib/test/test_email/test_email.py | 204 +++++++++++++++++-
...-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8 +
4 files changed, 361 insertions(+), 21 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
index 63fae2ab84..d1e1898591 100644
--- a/Doc/library/email.utils.rst
+++ b/Doc/library/email.utils.rst
@@ -60,13 +60,18 @@ of the new API.
begins with angle brackets, they are stripped off.
-.. function:: parseaddr(address)
+.. function:: parseaddr(address, *, strict=True)
Parse address -- which should be the value of some address-containing field such
as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
*email address* parts. Returns a tuple of that information, unless the parse
fails, in which case a 2-tuple of ``('', '')`` is returned.
+ If *strict* is true, use a strict parser which rejects malformed inputs.
+
+ .. versionchanged:: 3.13
+ Add *strict* optional parameter and reject malformed inputs by default.
+
.. function:: formataddr(pair, charset='utf-8')
@@ -84,12 +89,15 @@ of the new API.
Added the *charset* option.
-.. function:: getaddresses(fieldvalues)
+.. function:: getaddresses(fieldvalues, *, strict=True)
This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
*fieldvalues* is a sequence of header field values as might be returned by
- :meth:`Message.get_all <email.message.Message.get_all>`. Here's a simple
- example that gets all the recipients of a message::
+ :meth:`Message.get_all <email.message.Message.get_all>`.
+
+ If *strict* is true, use a strict parser which rejects malformed inputs.
+
+ Here's a simple example that gets all the recipients of a message::
from email.utils import getaddresses
@@ -99,6 +107,9 @@ of the new API.
resent_ccs = msg.get_all('resent-cc', [])
all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
+ .. versionchanged:: 3.13
+ Add *strict* optional parameter and reject malformed inputs by default.
+
.. function:: parsedate(date)
diff --git a/Lib/email/utils.py b/Lib/email/utils.py
index 39c2240607..f83b7e5d7e 100644
--- a/Lib/email/utils.py
+++ b/Lib/email/utils.py
@@ -48,6 +48,7 @@ TICK = "'"
specialsre = re.compile(r'[][\\()<>@,:;".]')
escapesre = re.compile(r'[\\"]')
+
def _has_surrogates(s):
"""Return True if s contains surrogate-escaped binary data."""
# This check is based on the fact that unless there are surrogates, utf8
@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
return address
+def _iter_escaped_chars(addr):
+ pos = 0
+ escape = False
+ for pos, ch in enumerate(addr):
+ if escape:
+ yield (pos, '\\' + ch)
+ escape = False
+ elif ch == '\\':
+ escape = True
+ else:
+ yield (pos, ch)
+ if escape:
+ yield (pos, '\\')
-def getaddresses(fieldvalues):
- """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
- all = COMMASPACE.join(fieldvalues)
- a = _AddressList(all)
- return a.addresslist
+
+def _strip_quoted_realnames(addr):
+ """Strip real names between quotes."""
+ if '"' not in addr:
+ # Fast path
+ return addr
+
+ start = 0
+ open_pos = None
+ result = []
+ for pos, ch in _iter_escaped_chars(addr):
+ if ch == '"':
+ if open_pos is None:
+ open_pos = pos
+ else:
+ if start != open_pos:
+ result.append(addr[start:open_pos])
+ start = pos + 1
+ open_pos = None
+
+ if start < len(addr):
+ result.append(addr[start:])
+
+ return ''.join(result)
+
+
+supports_strict_parsing = True
+
+def getaddresses(fieldvalues, *, strict=True):
+ """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
+
+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
+ its place.
+
+ If strict is true, use a strict parser which rejects malformed inputs.
+ """
+
+ # If strict is true, if the resulting list of parsed addresses is greater
+ # than the number of fieldvalues in the input list, a parsing error has
+ # occurred and consequently a list containing a single empty 2-tuple [('',
+ # '')] is returned in its place. This is done to avoid invalid output.
+ #
+ # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
+ # Safe output: [('', '')]
+
+ if not strict:
+ all = COMMASPACE.join(str(v) for v in fieldvalues)
+ a = _AddressList(all)
+ return a.addresslist
+
+ fieldvalues = [str(v) for v in fieldvalues]
+ fieldvalues = _pre_parse_validation(fieldvalues)
+ addr = COMMASPACE.join(fieldvalues)
+ a = _AddressList(addr)
+ result = _post_parse_validation(a.addresslist)
+
+ # Treat output as invalid if the number of addresses is not equal to the
+ # expected number of addresses.
+ n = 0
+ for v in fieldvalues:
+ # When a comma is used in the Real Name part it is not a deliminator.
+ # So strip those out before counting the commas.
+ v = _strip_quoted_realnames(v)
+ # Expected number of addresses: 1 + number of commas
+ n += 1 + v.count(',')
+ if len(result) != n:
+ return [('', '')]
+
+ return result
+
+
+def _check_parenthesis(addr):
+ # Ignore parenthesis in quoted real names.
+ addr = _strip_quoted_realnames(addr)
+
+ opens = 0
+ for pos, ch in _iter_escaped_chars(addr):
+ if ch == '(':
+ opens += 1
+ elif ch == ')':
+ opens -= 1
+ if opens < 0:
+ return False
+ return (opens == 0)
+
+
+def _pre_parse_validation(email_header_fields):
+ accepted_values = []
+ for v in email_header_fields:
+ if not _check_parenthesis(v):
+ v = "('', '')"
+ accepted_values.append(v)
+
+ return accepted_values
+
+
+def _post_parse_validation(parsed_email_header_tuples):
+ accepted_values = []
+ # The parser would have parsed a correctly formatted domain-literal
+ # The existence of an [ after parsing indicates a parsing failure
+ for v in parsed_email_header_tuples:
+ if '[' in v[1]:
+ v = ('', '')
+ accepted_values.append(v)
+
+ return accepted_values
@@ -214,16 +330,33 @@ def parsedate_to_datetime(data):
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
-def parseaddr(addr):
+def parseaddr(addr, *, strict=True):
"""
Parse addr into its constituent realname and email address parts.
Return a tuple of realname and email address, unless the parse fails, in
which case return a 2-tuple of ('', '').
+
+ If strict is True, use a strict parser which rejects malformed inputs.
"""
- addrs = _AddressList(addr).addresslist
- if not addrs:
- return '', ''
+ if not strict:
+ addrs = _AddressList(addr).addresslist
+ if not addrs:
+ return ('', '')
+ return addrs[0]
+
+ if isinstance(addr, list):
+ addr = addr[0]
+
+ if not isinstance(addr, str):
+ return ('', '')
+
+ addr = _pre_parse_validation([addr])[0]
+ addrs = _post_parse_validation(_AddressList(addr).addresslist)
+
+ if not addrs or len(addrs) > 1:
+ return ('', '')
+
return addrs[0]
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
index e4e40b612f..ce36efc1b1 100644
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -19,6 +19,7 @@ except ImportError:
import email
import email.policy
+import email.utils
from email.charset import Charset
from email.header import Header, decode_header, make_header
@@ -3207,15 +3208,154 @@ Foo
[('Al Person', 'aperson@dom.ain'),
('Bud Person', 'bperson@dom.ain')])
+ def test_getaddresses_comma_in_name(self):
+ """GH-106669 regression test."""
+ self.assertEqual(
+ utils.getaddresses(
+ [
+ '"Bud, Person" <bperson@dom.ain>',
+ 'aperson@dom.ain (Al Person)',
+ '"Mariusz Felisiak" <to@example.com>',
+ ]
+ ),
+ [
+ ('Bud, Person', 'bperson@dom.ain'),
+ ('Al Person', 'aperson@dom.ain'),
+ ('Mariusz Felisiak', 'to@example.com'),
+ ],
+ )
+
+ def test_parsing_errors(self):
+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
+ alice = 'alice@example.org'
+ bob = 'bob@example.com'
+ empty = ('', '')
+
+ # Test utils.getaddresses() and utils.parseaddr() on malformed email
+ # addresses: default behavior (strict=True) rejects malformed address,
+ # and strict=False which tolerates malformed address.
+ for invalid_separator, expected_non_strict in (
+ ('(', [(f'<{bob}>', alice)]),
+ (')', [('', alice), empty, ('', bob)]),
+ ('<', [('', alice), empty, ('', bob), empty]),
+ ('>', [('', alice), empty, ('', bob)]),
+ ('[', [('', f'{alice}[<{bob}>]')]),
+ (']', [('', alice), empty, ('', bob)]),
+ ('@', [empty, empty, ('', bob)]),
+ (';', [('', alice), empty, ('', bob)]),
+ (':', [('', alice), ('', bob)]),
+ ('.', [('', alice + '.'), ('', bob)]),
+ ('"', [('', alice), ('', f'<{bob}>')]),
+ ):
+ address = f'{alice}{invalid_separator}<{bob}>'
+ with self.subTest(address=address):
+ self.assertEqual(utils.getaddresses([address]),
+ [empty])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ expected_non_strict)
+
+ self.assertEqual(utils.parseaddr([address]),
+ empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Comma (',') is treated differently depending on strict parameter.
+ # Comma without quotes.
+ address = f'{alice},<{bob}>'
+ self.assertEqual(utils.getaddresses([address]),
+ [('', alice), ('', bob)])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ [('', alice), ('', bob)])
+ self.assertEqual(utils.parseaddr([address]),
+ empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Real name between quotes containing comma.
+ address = '"Alice, alice@example.org" <bob@example.com>'
+ expected_strict = ('Alice, alice@example.org', 'bob@example.com')
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Valid parenthesis in comments.
+ address = 'alice@example.org (Alice)'
+ expected_strict = ('Alice', 'alice@example.org')
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Invalid parenthesis in comments.
+ address = 'alice@example.org )Alice('
+ self.assertEqual(utils.getaddresses([address]), [empty])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
+ self.assertEqual(utils.parseaddr([address]), empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Two addresses with quotes separated by comma.
+ address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
+ self.assertEqual(utils.getaddresses([address]),
+ [('Jane Doe', 'jane@example.net'),
+ ('John Doe', 'john@example.net')])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ [('Jane Doe', 'jane@example.net'),
+ ('John Doe', 'john@example.net')])
+ self.assertEqual(utils.parseaddr([address]), empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Test email.utils.supports_strict_parsing attribute
+ self.assertEqual(email.utils.supports_strict_parsing, True)
+
def test_getaddresses_nasty(self):
- eq = self.assertEqual
- eq(utils.getaddresses(['foo: ;']), [('', '')])
- eq(utils.getaddresses(
- ['[]*-- =~$']),
- [('', ''), ('', ''), ('', '*--')])
- eq(utils.getaddresses(
- ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
- [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
+ for addresses, expected in (
+ (['"Sürname, Firstname" <to@example.com>'],
+ [('Sürname, Firstname', 'to@example.com')]),
+
+ (['foo: ;'],
+ [('', '')]),
+
+ (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
+ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
+
+ ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
+ [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
+
+ (['(Empty list)(start)Undisclosed recipients :(nobody(I know))'],
+ [('', '')]),
+
+ (['Mary <@machine.tld:mary@example.net>, , jdoe@test . example'],
+ [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
+
+ (['John Doe <jdoe@machine(comment). example>'],
+ [('John Doe (comment)', 'jdoe@machine.example')]),
+
+ (['"Mary Smith: Personal Account" <smith@home.example>'],
+ [('Mary Smith: Personal Account', 'smith@home.example')]),
+
+ (['Undisclosed recipients:;'],
+ [('', '')]),
+
+ ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
+ [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
+ ):
+ with self.subTest(addresses=addresses):
+ self.assertEqual(utils.getaddresses(addresses),
+ expected)
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
+ expected)
+
+ addresses = ['[]*-- =~$']
+ self.assertEqual(utils.getaddresses(addresses),
+ [('', '')])
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
+ [('', ''), ('', ''), ('', '*--')])
def test_getaddresses_embedded_comment(self):
"""Test proper handling of a nested comment"""
@@ -3397,6 +3537,54 @@ multipart/report
m = cls(*constructor, policy=email.policy.default)
self.assertIs(m.policy, email.policy.default)
+ def test_iter_escaped_chars(self):
+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
+ [(0, 'a'),
+ (2, '\\\\'),
+ (3, 'b'),
+ (5, '\\"'),
+ (6, 'c'),
+ (8, '\\\\'),
+ (9, '"'),
+ (10, 'd')])
+ self.assertEqual(list(utils._iter_escaped_chars('a\\')),
+ [(0, 'a'), (1, '\\')])
+
+ def test_strip_quoted_realnames(self):
+ def check(addr, expected):
+ self.assertEqual(utils._strip_quoted_realnames(addr), expected)
+
+ check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
+ ' <jane@example.net>, <john@example.net>')
+ check(r'"Jane \"Doe\"." <jane@example.net>',
+ ' <jane@example.net>')
+
+ # special cases
+ check(r'before"name"after', 'beforeafter')
+ check(r'before"name"', 'before')
+ check(r'b"name"', 'b') # single char
+ check(r'"name"after', 'after')
+ check(r'"name"a', 'a') # single char
+ check(r'"name"', '')
+
+ # no change
+ for addr in (
+ 'Jane Doe <jane@example.net>, John Doe <john@example.net>',
+ 'lone " quote',
+ ):
+ self.assertEqual(utils._strip_quoted_realnames(addr), addr)
+
+
+ def test_check_parenthesis(self):
+ addr = 'alice@example.net'
+ self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
+ self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
+ self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
+ self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
+
+ # Ignore real name between quotes
+ self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
+
# Test the iterator/generators
class TestIterators(TestEmailBase):
diff --git a/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
new file mode 100644
index 0000000000..3d0e9e4078
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
@@ -0,0 +1,8 @@
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
+return ``('', '')`` 2-tuples in more situations where invalid email
+addresses are encountered instead of potentially inaccurate values. Add
+optional *strict* parameter to these two functions: use ``strict=False`` to
+get the old behavior, accept malformed inputs.
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
+Stinner to improve the CVE-2023-27043 fix.
From 4df4fad359c280f2328b98ea9b4414f244624a58 Mon Sep 17 00:00:00 2001
From: Lumir Balhar <lbalhar@redhat.com>
Date: Mon, 18 Dec 2023 20:15:33 +0100
Subject: [PATCH] Make it possible to disable strict parsing in email module
---
Doc/library/email.utils.rst | 26 +++++++++++
Lib/email/utils.py | 54 ++++++++++++++++++++++-
Lib/test/test_email/test_email.py | 72 ++++++++++++++++++++++++++++++-
3 files changed, 149 insertions(+), 3 deletions(-)
diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
index d1e1898591..7aef773b5f 100644
--- a/Doc/library/email.utils.rst
+++ b/Doc/library/email.utils.rst
@@ -69,6 +69,19 @@ of the new API.
If *strict* is true, use a strict parser which rejects malformed inputs.
+ The default setting for *strict* is set to ``True``, but you can override
+ it by setting the environment variable ``PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING``
+ to non-empty string.
+
+ Additionally, you can permanently set the default value for *strict* to
+ ``False`` by creating the configuration file ``/etc/python/email.cfg``
+ with the following content:
+
+ .. code-block:: ini
+
+ [email_addr_parsing]
+ PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true
+
.. versionchanged:: 3.13
Add *strict* optional parameter and reject malformed inputs by default.
@@ -97,6 +110,19 @@ of the new API.
If *strict* is true, use a strict parser which rejects malformed inputs.
+ The default setting for *strict* is set to ``True``, but you can override
+ it by setting the environment variable ``PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING``
+ to non-empty string.
+
+ Additionally, you can permanently set the default value for *strict* to
+ ``False`` by creating the configuration file ``/etc/python/email.cfg``
+ with the following content:
+
+ .. code-block:: ini
+
+ [email_addr_parsing]
+ PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true
+
Here's a simple example that gets all the recipients of a message::
from email.utils import getaddresses
diff --git a/Lib/email/utils.py b/Lib/email/utils.py
index f83b7e5d7e..b8e90ceb8e 100644
--- a/Lib/email/utils.py
+++ b/Lib/email/utils.py
@@ -48,6 +48,46 @@ TICK = "'"
specialsre = re.compile(r'[][\\()<>@,:;".]')
escapesre = re.compile(r'[\\"]')
+_EMAIL_CONFIG_FILE = "/etc/python/email.cfg"
+_cached_strict_addr_parsing = None
+
+
+def _use_strict_email_parsing():
+ """"Cache implementation for _cached_strict_addr_parsing"""
+ global _cached_strict_addr_parsing
+ if _cached_strict_addr_parsing is None:
+ _cached_strict_addr_parsing = _use_strict_email_parsing_impl()
+ return _cached_strict_addr_parsing
+
+
+def _use_strict_email_parsing_impl():
+ """Returns True if strict email parsing is not disabled by
+ config file or env variable.
+ """
+ disabled = bool(os.environ.get("PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"))
+ if disabled:
+ return False
+
+ try:
+ file = open(_EMAIL_CONFIG_FILE)
+ except FileNotFoundError:
+ pass
+ else:
+ with file:
+ import configparser
+ config = configparser.ConfigParser(
+ interpolation=None,
+ comment_prefixes=('#', ),
+
+ )
+ config.read_file(file)
+ disabled = config.getboolean('email_addr_parsing', "PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING", fallback=None)
+
+ if disabled:
+ return False
+
+ return True
+
def _has_surrogates(s):
"""Return True if s contains surrogate-escaped binary data."""
@@ -149,7 +189,7 @@ def _strip_quoted_realnames(addr):
supports_strict_parsing = True
-def getaddresses(fieldvalues, *, strict=True):
+def getaddresses(fieldvalues, *, strict=None):
"""Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
@@ -158,6 +198,11 @@ def getaddresses(fieldvalues, *, strict=True):
If strict is true, use a strict parser which rejects malformed inputs.
"""
+ # If default is used, it's True unless disabled
+ # by env variable or config file.
+ if strict == None:
+ strict = _use_strict_email_parsing()
+
# If strict is true, if the resulting list of parsed addresses is greater
# than the number of fieldvalues in the input list, a parsing error has
# occurred and consequently a list containing a single empty 2-tuple [('',
@@ -330,7 +375,7 @@ def parsedate_to_datetime(data):
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
-def parseaddr(addr, *, strict=True):
+def parseaddr(addr, *, strict=None):
"""
Parse addr into its constituent realname and email address parts.
@@ -339,6 +384,11 @@ def parseaddr(addr, *, strict=True):
If strict is True, use a strict parser which rejects malformed inputs.
"""
+ # If default is used, it's True unless disabled
+ # by env variable or config file.
+ if strict == None:
+ strict = _use_strict_email_parsing()
+
if not strict:
addrs = _AddressList(addr).addresslist
if not addrs:
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
index ce36efc1b1..05ea201b68 100644
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -7,6 +7,9 @@ import time
import base64
import unittest
import textwrap
+import contextlib
+import tempfile
+import os
from io import StringIO, BytesIO
from itertools import chain
@@ -41,7 +44,7 @@ from email import iterators
from email import base64mime
from email import quoprimime
-from test.support import unlink, start_threads
+from test.support import unlink, start_threads, EnvironmentVarGuard, swap_attr
from test.test_email import openfile, TestEmailBase
# These imports are documented to work, but we are testing them using a
@@ -3313,6 +3316,73 @@ Foo
# Test email.utils.supports_strict_parsing attribute
self.assertEqual(email.utils.supports_strict_parsing, True)
+ def test_parsing_errors_strict_set_via_env_var(self):
+ address = 'alice@example.org )Alice('
+ empty = ('', '')
+
+ # Reset cached default value to make the function
+ # reload the config file provided below.
+ utils._cached_strict_addr_parsing = None
+
+ # Strict disabled via env variable, old behavior expected
+ with EnvironmentVarGuard() as environ:
+ environ["PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"] = "1"
+
+ self.assertEqual(utils.getaddresses([address]),
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
+ self.assertEqual(utils.parseaddr([address]), ('', address))
+
+ # Clear cache again
+ utils._cached_strict_addr_parsing = None
+
+ # Default strict=True, empty result expected
+ self.assertEqual(utils.getaddresses([address]), [empty])
+ self.assertEqual(utils.parseaddr([address]), empty)
+
+ # Clear cache again
+ utils._cached_strict_addr_parsing = None
+
+ # Empty string in env variable = strict parsing enabled (default)
+ with EnvironmentVarGuard() as environ:
+ environ["PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING"] = ""
+
+ # Default strict=True, empty result expected
+ self.assertEqual(utils.getaddresses([address]), [empty])
+ self.assertEqual(utils.parseaddr([address]), empty)
+
+ @contextlib.contextmanager
+ def _email_strict_parsing_conf(self):
+ """Context for the given email strict parsing configured in config file"""
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ filename = os.path.join(tmpdirname, 'conf.cfg')
+ with swap_attr(utils, "_EMAIL_CONFIG_FILE", filename):
+ with open(filename, 'w') as file:
+ file.write('[email_addr_parsing]\n')
+ file.write('PYTHON_EMAIL_DISABLE_STRICT_ADDR_PARSING = true')
+ utils._EMAIL_CONFIG_FILE = filename
+ yield
+
+ def test_parsing_errors_strict_disabled_via_config_file(self):
+ address = 'alice@example.org )Alice('
+ empty = ('', '')
+
+ # Reset cached default value to make the function
+ # reload the config file provided below.
+ utils._cached_strict_addr_parsing = None
+
+ # Strict disabled via config file, old results expected
+ with self._email_strict_parsing_conf():
+ self.assertEqual(utils.getaddresses([address]),
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
+ self.assertEqual(utils.parseaddr([address]), ('', address))
+
+ # Clear cache again
+ utils._cached_strict_addr_parsing = None
+
+ # Default strict=True, empty result expected
+ self.assertEqual(utils.getaddresses([address]), [empty])
+ self.assertEqual(utils.parseaddr([address]), empty)
+
def test_getaddresses_nasty(self):
for addresses, expected in (
(['"Sürname, Firstname" <to@example.com>'],
--
2.43.0

View File

@ -14,7 +14,7 @@ URL: https://www.python.org/
# WARNING When rebasing to a new Python version,
# remember to update the python3-docs package as well
Version: %{pybasever}.8
Release: 56%{?dist}.3
Release: 51%{?dist}
License: Python
@ -765,78 +765,6 @@ Patch387: 00387-cve-2020-10735-prevent-dos-by-very-large-int.patch
# the behavior to linear.
Patch394: 00394-cve-2022-45061-cpu-denial-of-service-via-inefficient-idna-decoder.patch
# 00397 #
# Add filters for tarfile extraction (CVE-2007-4559, PEP-706)
# The first patches in the file backport the upstream fix:
# - https://github.com/python/cpython/pull/104583
# (see the linked issue for merged backports)
# Next-to-last patch fixes determination of symlink targets, which were treated
# as relative to the root of the archive,
# rather than the directory containing the symlink.
# Not yet upstream as of this writing.
# The last patch is Red Hat configuration, see KB for documentation:
# - https://access.redhat.com/articles/7004769
Patch397: 00397-tarfile-filter.patch
# 00399 #
# CVE-2023-24329
#
# gh-102153: Start stripping C0 control and space chars in `urlsplit` (GH-102508)
#
# `urllib.parse.urlsplit` has already been respecting the WHATWG spec a bit GH-25595.
#
# This adds more sanitizing to respect the "Remove any leading C0 control or space from input" [rule](https://url.spec.whatwg.org/GH-url-parsing:~:text=Remove%%20any%%20leading%%20and%%20trailing%%20C0%%20control%%20or%%20space%%20from%%20input.) in response to [CVE-2023-24329](https://nvd.nist.gov/vuln/detail/CVE-2023-24329).
#
# Backported from Python 3.12
Patch399: 00399-cve-2023-24329.patch
# 00404 #
# CVE-2023-40217
#
# Security fix for CVE-2023-40217: Bypass TLS handshake on closed sockets
# Resolved upstream: https://github.com/python/cpython/issues/108310
# Fixups added on top:
# https://github.com/python/cpython/pull/108352
# https://github.com/python/cpython/pull/108408
#
# Backported from Python 3.8
Patch404: 00404-cve-2023-40217.patch
# 00408 #
# CVE-2022-48560
#
# Security fix for CVE-2022-48560: python3: use after free in heappushpop()
# of heapq module
# Resolved upstream: https://github.com/python/cpython/issues/83602
Patch408: 00408-CVE-2022-48560.patch
# 00413 #
# CVE-2022-48564
#
# DoS when processing malformed Apple Property List files in binary format
# Resolved upstream: https://github.com/python/cpython/commit/a63234c49b2fbfb6f0aca32525e525ce3d43b2b4
Patch413: 00413-CVE-2022-48564.patch
# 00414 #
#
# Skip test_pair() and test_speech128() of test_zlib on s390x since
# they fail if zlib uses the s390x hardware accelerator.
Patch414: 00414-skip_test_zlib_s390x.patch
# 00415 #
# [CVE-2023-27043] gh-102988: Reject malformed addresses in email.parseaddr() (#111116)
#
# Detect email address parsing errors and return empty tuple to
# indicate the parsing error (old API). Add an optional 'strict'
# parameter to getaddresses() and parseaddr() functions. Patch by
# Thomas Dwyer.
#
# Upstream PR: https://github.com/python/cpython/pull/111116
#
# Second patch implmenets the possibility to restore the old behavior via
# config file or environment variable.
Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch
# (New patches go here ^^^)
#
# When adding new patches to "python" and "python3" in Fedora, EL, etc.,
@ -1051,7 +979,6 @@ configuration, browsers, and other dialogs.
%package tkinter
Summary: A GUI toolkit for Python
Requires: platform-python = %{version}-%{release}
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
%description tkinter
The Tkinter (Tk interface) library is a graphical user interface toolkit for
@ -1183,13 +1110,6 @@ git apply %{PATCH351}
%patch386 -p1
%patch387 -p1
%patch394 -p1
%patch397 -p1
%patch399 -p1
%patch404 -p1
%patch408 -p1
%patch413 -p1
%patch414 -p1
%patch415 -p1
# Remove files that should be generated by the build
# (This is after patching, so that we can use patches directly from upstream)
@ -2121,40 +2041,6 @@ fi
# ======================================================
%changelog
* Fri Jan 05 2024 Lumír Balhar <lbalhar@redhat.com> - 3.6.8-56.3
- Security fix for CVE-2023-27043
Resolves: RHEL-5563
* Tue Dec 12 2023 Lumír Balhar <lbalhar@redhat.com> - 3.6.8-56.2
- Security fix for CVE-2022-48564
Resolves: RHEL-16673
- Skip tests failing on s390x
Resolves: RHEL-19251
* Thu Nov 23 2023 Lumír Balhar <lbalhar@redhat.com> - 3.6.8-56.1
- Security fix for CVE-2022-48560
Resolves: RHEL-16706
* Thu Sep 07 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.6.8-56
- Security fix for CVE-2023-40217
Resolves: RHEL-3041
* Wed Aug 09 2023 Petr Viktorin <pviktori@redhat.com> - 3.6.8-55
- Fix symlink handling in the fix for CVE-2007-4559
Resolves: rhbz#263261
* Fri Jul 07 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.6.8-54
- Bump release for rebuild
Resolves: rhbz#2173917
* Fri Jun 30 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.6.8-53
- Security fix for CVE-2023-24329
Resolves: rhbz#2173917
* Tue Jun 06 2023 Petr Viktorin <pviktori@redhat.com> - 3.6.8-52
- Add filters for tarfile extraction (CVE-2007-4559, PEP-706)
Resolves: rhbz#263261
* Tue Jan 24 2023 Charalampos Stratakis <cstratak@redhat.com> - 3.6.8-51
- Properly strip the LTO bytecode from python.o
Resolves: rhbz#2137707