import CS python3-3.6.8-59.el8
This commit is contained in:
parent
b879da37a4
commit
75b52ffd3f
648
SOURCES/00404-cve-2023-40217.patch
Normal file
648
SOURCES/00404-cve-2023-40217.patch
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
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
|
||||||
|
|
143
SOURCES/00408-CVE-2022-48560.patch
Normal file
143
SOURCES/00408-CVE-2022-48560.patch
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
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 */
|
560
SOURCES/00413-CVE-2022-48564.patch
Normal file
560
SOURCES/00413-CVE-2022-48564.patch
Normal file
@ -0,0 +1,560 @@
|
|||||||
|
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
|
||||||
|
|
88
SOURCES/00414-skip_test_zlib_s390x.patch
Normal file
88
SOURCES/00414-skip_test_zlib_s390x.patch
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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
|
||||||
|
|
@ -0,0 +1,750 @@
|
|||||||
|
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
|
||||||
|
|
@ -14,7 +14,7 @@ URL: https://www.python.org/
|
|||||||
# WARNING When rebasing to a new Python version,
|
# WARNING When rebasing to a new Python version,
|
||||||
# remember to update the python3-docs package as well
|
# remember to update the python3-docs package as well
|
||||||
Version: %{pybasever}.8
|
Version: %{pybasever}.8
|
||||||
Release: 55%{?dist}
|
Release: 59%{?dist}
|
||||||
License: Python
|
License: Python
|
||||||
|
|
||||||
|
|
||||||
@ -790,6 +790,53 @@ Patch397: 00397-tarfile-filter.patch
|
|||||||
# Backported from Python 3.12
|
# Backported from Python 3.12
|
||||||
Patch399: 00399-cve-2023-24329.patch
|
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 ^^^)
|
# (New patches go here ^^^)
|
||||||
#
|
#
|
||||||
# When adding new patches to "python" and "python3" in Fedora, EL, etc.,
|
# When adding new patches to "python" and "python3" in Fedora, EL, etc.,
|
||||||
@ -1138,6 +1185,11 @@ git apply %{PATCH351}
|
|||||||
%patch394 -p1
|
%patch394 -p1
|
||||||
%patch397 -p1
|
%patch397 -p1
|
||||||
%patch399 -p1
|
%patch399 -p1
|
||||||
|
%patch404 -p1
|
||||||
|
%patch408 -p1
|
||||||
|
%patch413 -p1
|
||||||
|
%patch414 -p1
|
||||||
|
%patch415 -p1
|
||||||
|
|
||||||
# Remove files that should be generated by the build
|
# Remove files that should be generated by the build
|
||||||
# (This is after patching, so that we can use patches directly from upstream)
|
# (This is after patching, so that we can use patches directly from upstream)
|
||||||
@ -2069,6 +2121,24 @@ fi
|
|||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Thu Jan 04 2024 Lumír Balhar <lbalhar@redhat.com> - 3.6.8-59
|
||||||
|
- Security fix for CVE-2023-27043
|
||||||
|
Resolves: RHEL-20610
|
||||||
|
|
||||||
|
* Tue Dec 12 2023 Lumír Balhar <lbalhar@redhat.com> - 3.6.8-58
|
||||||
|
- Security fix for CVE-2022-48564
|
||||||
|
Resolves: RHEL-16674
|
||||||
|
- Skip tests failing on s390x
|
||||||
|
Resolves: RHEL-19252
|
||||||
|
|
||||||
|
* Thu Nov 23 2023 Lumír Balhar <lbalhar@redhat.com> - 3.6.8-57
|
||||||
|
- Security fix for CVE-2022-48560
|
||||||
|
Resolves: RHEL-16707
|
||||||
|
|
||||||
|
* 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
|
* Wed Aug 09 2023 Petr Viktorin <pviktori@redhat.com> - 3.6.8-55
|
||||||
- Fix symlink handling in the fix for CVE-2007-4559
|
- Fix symlink handling in the fix for CVE-2007-4559
|
||||||
Resolves: rhbz#263261
|
Resolves: rhbz#263261
|
||||||
|
Loading…
Reference in New Issue
Block a user