Bug#1089071: bookworm-pu: package python3.11/3.11.2-6+deb12u5
Package: release.debian.org
Severity: normal
Tags: bookworm moreinfo
User: release.debian.org@packages.debian.org
Usertags: pu
X-Debbugs-Cc: security@debian.org, Matthias Klose <doko@debian.org>
* CVE-2023-27043: Reject malformed addresses in email.parseaddr()
(Closes: #1059298)
* CVE-2024-6923: Encode newlines in headers in the email module
* CVE-2024-7592: Quadratic complexity parsing cookies with backslashes
* CVE-2024-9287: venv activation scripts did't quote paths
* CVE-2024-11168: urllib functions improperly validated bracketed hosts
Tagged moreinfo, as question to the security team whether they want
this in -pu or as DSA.
diffstat for python3.11-3.11.2 python3.11-3.11.2
changelog | 12
patches/0001-3.11-CVE-2023-27043-gh-102988-Reject-malformed-addre.patch | 482 ++++++++++
patches/0002-3.11-gh-121650-Encode-newlines-in-headers-and-verify.patch | 326 ++++++
patches/0003-3.11-gh-123067-Fix-quadratic-complexity-in-parsing-q.patch | 127 ++
patches/0004-3.11-gh-124651-Quote-template-strings-in-venv-activa.patch | 281 +++++
patches/0005-3.11-gh-103848-Adds-checks-to-ensure-that-bracketed-.patch | 108 ++
patches/series | 5
7 files changed, 1341 insertions(+)
diff -Nru python3.11-3.11.2/debian/changelog python3.11-3.11.2/debian/changelog
--- python3.11-3.11.2/debian/changelog 2024-09-14 06:00:30.000000000 +0300
+++ python3.11-3.11.2/debian/changelog 2024-11-30 23:22:50.000000000 +0200
@@ -1,3 +1,15 @@
+python3.11 (3.11.2-6+deb12u5) bookworm; urgency=medium
+
+ * Non-maintainer upload.
+ * CVE-2023-27043: Reject malformed addresses in email.parseaddr()
+ (Closes: #1059298)
+ * CVE-2024-6923: Encode newlines in headers in the email module
+ * CVE-2024-7592: Quadratic complexity parsing cookies with backslashes
+ * CVE-2024-9287: venv activation scripts did't quote paths
+ * CVE-2024-11168: urllib functions improperly validated bracketed hosts
+
+ -- Adrian Bunk <bunk@debian.org> Sat, 30 Nov 2024 23:22:50 +0200
+
python3.11 (3.11.2-6+deb12u4) bookworm; urgency=medium
* Fix zipfile.Path regression introduced by 3.11.2-6+deb12u3
diff -Nru python3.11-3.11.2/debian/patches/0001-3.11-CVE-2023-27043-gh-102988-Reject-malformed-addre.patch python3.11-3.11.2/debian/patches/0001-3.11-CVE-2023-27043-gh-102988-Reject-malformed-addre.patch
--- python3.11-3.11.2/debian/patches/0001-3.11-CVE-2023-27043-gh-102988-Reject-malformed-addre.patch 1970-01-01 02:00:00.000000000 +0200
+++ python3.11-3.11.2/debian/patches/0001-3.11-CVE-2023-27043-gh-102988-Reject-malformed-addre.patch 2024-11-30 23:22:50.000000000 +0200
@@ -0,0 +1,482 @@
+From f4dc686df26f5f8616b695dc5850ba127e334d3e Mon Sep 17 00:00:00 2001
+From: Petr Viktorin <encukou@gmail.com>
+Date: Fri, 6 Sep 2024 12:46:23 +0200
+Subject: [3.11] [CVE-2023-27043] gh-102988: Reject malformed addresses in
+ email.parseaddr() (GH-111116) (#123767)
+
+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.
+
+(cherry picked from commit 4a153a1d3b18803a684cd1bcc2cdf3ede3dbae19)
+
+Co-authored-by: Victor Stinner <vstinner@python.org>
+Co-authored-by: Thomas Dwyer <github@tomd.tel>
+---
+ Doc/library/email.utils.rst | 19 ++-
+ Lib/email/utils.py | 150 ++++++++++++++++++++--
+ Lib/test/test_email/test_email.py | 204 ++++++++++++++++++++++++++++--
+ 3 files changed, 352 insertions(+), 21 deletions(-)
+
+diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
+index 0e266b6a457..7b02184a9d8 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.11.2-6+deb12u5
++ 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.11.2-6+deb12u5
++ 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 cfdfeb3f1a8..940b365a574 100644
+--- a/Lib/email/utils.py
++++ b/Lib/email/utils.py
+@@ -106,12 +106,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 _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.
+
+-def getaddresses(fieldvalues):
+- """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
+- all = COMMASPACE.join(str(v) for v in fieldvalues)
+- a = _AddressList(all)
+- return a.addresslist
++ 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
+
+
+ def _format_timetuple_and_zone(timetuple, zone):
+@@ -205,16 +320,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 ca8212825ff..4a2fd13ba4c 100644
+--- a/Lib/test/test_email/test_email.py
++++ b/Lib/test/test_email/test_email.py
+@@ -17,6 +17,7 @@
+
+ import email
+ import email.policy
++import email.utils
+
+ from email.charset import Charset
+ from email.generator import Generator, DecodedGenerator, BytesGenerator
+@@ -3320,15 +3321,154 @@ def test_getaddresses(self):
+ [('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"""
+@@ -3518,6 +3658,54 @@ def test_mime_classes_policy_argument(self):
+ 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):
+--
+2.30.2
+
diff -Nru python3.11-3.11.2/debian/patches/0002-3.11-gh-121650-Encode-newlines-in-headers-and-verify.patch python3.11-3.11.2/debian/patches/0002-3.11-gh-121650-Encode-newlines-in-headers-and-verify.patch
--- python3.11-3.11.2/debian/patches/0002-3.11-gh-121650-Encode-newlines-in-headers-and-verify.patch 1970-01-01 02:00:00.000000000 +0200
+++ python3.11-3.11.2/debian/patches/0002-3.11-gh-121650-Encode-newlines-in-headers-and-verify.patch 2024-11-30 23:22:50.000000000 +0200
@@ -0,0 +1,326 @@
+From 61793762daa355dc7a2b5edd104ef38efb3efb2a Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?=C5=81ukasz=20Langa?= <lukasz@langa.pl>
+Date: Wed, 4 Sep 2024 17:37:28 +0200
+Subject: [3.11] gh-121650: Encode newlines in headers, and verify headers are
+ sound (GH-122233) (#122608)
+
+Per RFC 2047:
+
+> [...] these encoding schemes allow the
+> encoding of arbitrary octet values, mail readers that implement this
+> decoding should also ensure that display of the decoded data on the
+> recipient's terminal will not cause unwanted side-effects
+
+It seems that the "quoted-word" scheme is a valid way to include
+a newline character in a header value, just like we already allow
+undecodable bytes or control characters.
+They do need to be properly quoted when serialized to text, though.
+
+Verify that email headers are well-formed.
+
+This should fail for custom fold() implementations that aren't careful
+about newlines.
+
+(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
+
+Co-authored-by: Petr Viktorin <encukou@gmail.com>
+Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
+Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
+---
+ Doc/library/email.errors.rst | 5 +++
+ Doc/library/email.policy.rst | 18 ++++++++
+ Lib/email/_header_value_parser.py | 12 ++++--
+ Lib/email/_policybase.py | 8 ++++
+ Lib/email/errors.py | 4 ++
+ Lib/email/generator.py | 13 +++++-
+ Lib/test/test_email/test_generator.py | 62 +++++++++++++++++++++++++++
+ Lib/test/test_email/test_policy.py | 26 +++++++++++
+ 8 files changed, 144 insertions(+), 4 deletions(-)
+
+diff --git a/Doc/library/email.errors.rst b/Doc/library/email.errors.rst
+index 194a98696f4..a98a2777310 100644
+--- a/Doc/library/email.errors.rst
++++ b/Doc/library/email.errors.rst
+@@ -58,6 +58,11 @@ The following exception classes are defined in the :mod:`email.errors` module:
+ :class:`~email.mime.nonmultipart.MIMENonMultipart` (e.g.
+ :class:`~email.mime.image.MIMEImage`).
+
++.. exception:: HeaderWriteError()
++
++ Raised when an error occurs when the :mod:`~email.generator` outputs
++ headers.
++
+
+ Here is the list of the defects that the :class:`~email.parser.FeedParser`
+ can find while parsing messages. Note that the defects are added to the message
+diff --git a/Doc/library/email.policy.rst b/Doc/library/email.policy.rst
+index bf53b9520fc..f5ed3d20b0e 100644
+--- a/Doc/library/email.policy.rst
++++ b/Doc/library/email.policy.rst
+@@ -229,6 +229,24 @@ added matters. To illustrate::
+
+ .. versionadded:: 3.6
+
++
++ .. attribute:: verify_generated_headers
++
++ If ``True`` (the default), the generator will raise
++ :exc:`~email.errors.HeaderWriteError` instead of writing a header
++ that is improperly folded or delimited, such that it would
++ be parsed as multiple headers or joined with adjacent data.
++ Such headers can be generated by custom header classes or bugs
++ in the ``email`` module.
++
++ As it's a security feature, this defaults to ``True`` even in the
++ :class:`~email.policy.Compat32` policy.
++ For backwards compatible, but unsafe, behavior, it must be set to
++ ``False`` explicitly.
++
++ .. versionadded:: 3.11.2-6+deb12u5
++
++
+ The following :class:`Policy` method is intended to be called by code using
+ the email library to create policy instances with custom settings:
+
+diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py
+index e637e6df066..e1b99d5b417 100644
+--- a/Lib/email/_header_value_parser.py
++++ b/Lib/email/_header_value_parser.py
+@@ -92,6 +92,8 @@
+ ASPECIALS = TSPECIALS | set("*'%")
+ ATTRIBUTE_ENDS = ASPECIALS | WSP
+ EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS - set('%')
++NLSET = {'\n', '\r'}
++SPECIALSNL = SPECIALS | NLSET
+
+ def quote_string(value):
+ return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"'
+@@ -2778,9 +2780,13 @@ def _refold_parse_tree(parse_tree, *, policy):
+ wrap_as_ew_blocked -= 1
+ continue
+ tstr = str(part)
+- if part.token_type == 'ptext' and set(tstr) & SPECIALS:
+- # Encode if tstr contains special characters.
+- want_encoding = True
++ if not want_encoding:
++ if part.token_type == 'ptext':
++ # Encode if tstr contains special characters.
++ want_encoding = not SPECIALSNL.isdisjoint(tstr)
++ else:
++ # Encode if tstr contains newlines.
++ want_encoding = not NLSET.isdisjoint(tstr)
+ try:
+ tstr.encode(encoding)
+ charset = encoding
+diff --git a/Lib/email/_policybase.py b/Lib/email/_policybase.py
+index c9cbadd2a80..d1f48211f90 100644
+--- a/Lib/email/_policybase.py
++++ b/Lib/email/_policybase.py
+@@ -157,6 +157,13 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
+ message_factory -- the class to use to create new message objects.
+ If the value is None, the default is Message.
+
++ verify_generated_headers
++ -- if true, the generator verifies that each header
++ they are properly folded, so that a parser won't
++ treat it as multiple headers, start-of-body, or
++ part of another header.
++ This is a check against custom Header & fold()
++ implementations.
+ """
+
+ raise_on_defect = False
+@@ -165,6 +172,7 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
+ max_line_length = 78
+ mangle_from_ = False
+ message_factory = None
++ verify_generated_headers = True
+
+ def handle_defect(self, obj, defect):
+ """Based on policy, either raise defect or call register_defect.
+diff --git a/Lib/email/errors.py b/Lib/email/errors.py
+index 3ad00565549..02aa5eced6a 100644
+--- a/Lib/email/errors.py
++++ b/Lib/email/errors.py
+@@ -29,6 +29,10 @@ class CharsetError(MessageError):
+ """An illegal charset was given."""
+
+
++class HeaderWriteError(MessageError):
++ """Error while writing headers."""
++
++
+ # These are parsing defects which the parser was able to work around.
+ class MessageDefect(ValueError):
+ """Base class for a message defect."""
+diff --git a/Lib/email/generator.py b/Lib/email/generator.py
+index c9b121624e0..89224ae41cb 100644
+--- a/Lib/email/generator.py
++++ b/Lib/email/generator.py
+@@ -14,12 +14,14 @@
+ from copy import deepcopy
+ from io import StringIO, BytesIO
+ from email.utils import _has_surrogates
++from email.errors import HeaderWriteError
+
+ UNDERSCORE = '_'
+ NL = '\n' # XXX: no longer used by the code below.
+
+ NLCRE = re.compile(r'\r\n|\r|\n')
+ fcre = re.compile(r'^From ', re.MULTILINE)
++NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
+
+
+
+@@ -223,7 +225,16 @@ def _dispatch(self, msg):
+
+ def _write_headers(self, msg):
+ for h, v in msg.raw_items():
+- self.write(self.policy.fold(h, v))
++ folded = self.policy.fold(h, v)
++ if self.policy.verify_generated_headers:
++ linesep = self.policy.linesep
++ if not folded.endswith(self.policy.linesep):
++ raise HeaderWriteError(
++ f'folded header does not end with {linesep!r}: {folded!r}')
++ if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)):
++ raise HeaderWriteError(
++ f'folded header contains newline: {folded!r}')
++ self.write(folded)
+ # A blank line always separates headers from body
+ self.write(self._NL)
+
+diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py
+index 89e7edeb63a..d29400f0ed1 100644
+--- a/Lib/test/test_email/test_generator.py
++++ b/Lib/test/test_email/test_generator.py
+@@ -6,6 +6,7 @@
+ from email.generator import Generator, BytesGenerator
+ from email.headerregistry import Address
+ from email import policy
++import email.errors
+ from test.test_email import TestEmailBase, parameterize
+
+
+@@ -216,6 +217,44 @@ def test_rfc2231_wrapping_switches_to_default_len_if_too_narrow(self):
+ g.flatten(msg)
+ self.assertEqual(s.getvalue(), self.typ(expected))
+
++ def test_keep_encoded_newlines(self):
++ msg = self.msgmaker(self.typ(textwrap.dedent("""\
++ To: nobody
++ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
++
++ None
++ """)))
++ expected = textwrap.dedent("""\
++ To: nobody
++ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
++
++ None
++ """)
++ s = self.ioclass()
++ g = self.genclass(s, policy=self.policy.clone(max_line_length=80))
++ g.flatten(msg)
++ self.assertEqual(s.getvalue(), self.typ(expected))
++
++ def test_keep_long_encoded_newlines(self):
++ msg = self.msgmaker(self.typ(textwrap.dedent("""\
++ To: nobody
++ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
++
++ None
++ """)))
++ expected = textwrap.dedent("""\
++ To: nobody
++ Subject: Bad subject
++ =?utf-8?q?=0A?=Bcc:
++ injection@example.com
++
++ None
++ """)
++ s = self.ioclass()
++ g = self.genclass(s, policy=self.policy.clone(max_line_length=30))
++ g.flatten(msg)
++ self.assertEqual(s.getvalue(), self.typ(expected))
++
+
+ class TestGenerator(TestGeneratorBase, TestEmailBase):
+
+@@ -224,6 +263,29 @@ class TestGenerator(TestGeneratorBase, TestEmailBase):
+ ioclass = io.StringIO
+ typ = str
+
++ def test_verify_generated_headers(self):
++ """gh-121650: by default the generator prevents header injection"""
++ class LiteralHeader(str):
++ name = 'Header'
++ def fold(self, **kwargs):
++ return self
++
++ for text in (
++ 'Value\r\nBad Injection\r\n',
++ 'NoNewLine'
++ ):
++ with self.subTest(text=text):
++ message = message_from_string(
++ "Header: Value\r\n\r\nBody",
++ policy=self.policy,
++ )
++
++ del message['Header']
++ message['Header'] = LiteralHeader(text)
++
++ with self.assertRaises(email.errors.HeaderWriteError):
++ message.as_string()
++
+
+ class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
+
+diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py
+index e87c2755494..ff1ddf7d7a8 100644
+--- a/Lib/test/test_email/test_policy.py
++++ b/Lib/test/test_email/test_policy.py
+@@ -26,6 +26,7 @@ class PolicyAPITests(unittest.TestCase):
+ 'raise_on_defect': False,
+ 'mangle_from_': True,
+ 'message_factory': None,
++ 'verify_generated_headers': True,
+ }
+ # These default values are the ones set on email.policy.default.
+ # If any of these defaults change, the docs must be updated.
+@@ -277,6 +278,31 @@ def test_short_maxlen_error(self):
+ with self.assertRaises(email.errors.HeaderParseError):
+ policy.fold("Subject", subject)
+
++ def test_verify_generated_headers(self):
++ """Turning protection off allows header injection"""
++ policy = email.policy.default.clone(verify_generated_headers=False)
++ for text in (
++ 'Header: Value\r\nBad: Injection\r\n',
++ 'Header: NoNewLine'
++ ):
++ with self.subTest(text=text):
++ message = email.message_from_string(
++ "Header: Value\r\n\r\nBody",
++ policy=policy,
++ )
++ class LiteralHeader(str):
++ name = 'Header'
++ def fold(self, **kwargs):
++ return self
++
++ del message['Header']
++ message['Header'] = LiteralHeader(text)
++
++ self.assertEqual(
++ message.as_string(),
++ f"{text}\nBody",
++ )
++
+ # XXX: Need subclassing tests.
+ # For adding subclassed objects, make sure the usual rules apply (subclass
+ # wins), but that the order still works (right overrides left).
+--
+2.30.2
+
diff -Nru python3.11-3.11.2/debian/patches/0003-3.11-gh-123067-Fix-quadratic-complexity-in-parsing-q.patch python3.11-3.11.2/debian/patches/0003-3.11-gh-123067-Fix-quadratic-complexity-in-parsing-q.patch
--- python3.11-3.11.2/debian/patches/0003-3.11-gh-123067-Fix-quadratic-complexity-in-parsing-q.patch 1970-01-01 02:00:00.000000000 +0200
+++ python3.11-3.11.2/debian/patches/0003-3.11-gh-123067-Fix-quadratic-complexity-in-parsing-q.patch 2024-11-30 23:22:50.000000000 +0200
@@ -0,0 +1,127 @@
+From 8c14bb1657119a1026bd68f90da1b80292e0302d Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Wed, 4 Sep 2024 17:50:00 +0200
+Subject: [3.11] gh-123067: Fix quadratic complexity in parsing "-quoted cookie
+ values with backslashes (GH-123075) (#123105)
+
+This fixes CVE-2024-7592.
+(cherry picked from commit 44e458357fca05ca0ae2658d62c8c595b048b5ef)
+
+Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
+---
+ Lib/http/cookies.py | 34 ++++++++-----------------------
+ Lib/test/test_http_cookies.py | 38 +++++++++++++++++++++++++++++++++++
+ 2 files changed, 46 insertions(+), 26 deletions(-)
+
+diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
+index 35ac2dc6ae2..2c1f021d0ab 100644
+--- a/Lib/http/cookies.py
++++ b/Lib/http/cookies.py
+@@ -184,8 +184,13 @@ def _quote(str):
+ return '"' + str.translate(_Translator) + '"'
+
+
+-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
+-_QuotePatt = re.compile(r"[\\].")
++_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub
++
++def _unquote_replace(m):
++ if m[1]:
++ return chr(int(m[1], 8))
++ else:
++ return m[2]
+
+ def _unquote(str):
+ # If there aren't any doublequotes,
+@@ -205,30 +210,7 @@ def _unquote(str):
+ # \012 --> \n
+ # \" --> "
+ #
+- i = 0
+- n = len(str)
+- res = []
+- while 0 <= i < n:
+- o_match = _OctalPatt.search(str, i)
+- q_match = _QuotePatt.search(str, i)
+- if not o_match and not q_match: # Neither matched
+- res.append(str[i:])
+- break
+- # else:
+- j = k = -1
+- if o_match:
+- j = o_match.start(0)
+- if q_match:
+- k = q_match.start(0)
+- if q_match and (not o_match or k < j): # QuotePatt matched
+- res.append(str[i:k])
+- res.append(str[k+1])
+- i = k + 2
+- else: # OctalPatt matched
+- res.append(str[i:j])
+- res.append(chr(int(str[j+1:j+4], 8)))
+- i = j + 4
+- return _nulljoin(res)
++ return _unquote_sub(_unquote_replace, str)
+
+ # The _getdate() routine is used to set the expiration time in the cookie's HTTP
+ # header. By default, _getdate() returns the current time in the appropriate
+diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
+index 925c8697f60..8879902a6e2 100644
+--- a/Lib/test/test_http_cookies.py
++++ b/Lib/test/test_http_cookies.py
+@@ -5,6 +5,7 @@
+ import doctest
+ from http import cookies
+ import pickle
++from test import support
+
+
+ class CookieTests(unittest.TestCase):
+@@ -58,6 +59,43 @@ def test_basic(self):
+ for k, v in sorted(case['dict'].items()):
+ self.assertEqual(C[k].value, v)
+
++ def test_unquote(self):
++ cases = [
++ (r'a="b=\""', 'b="'),
++ (r'a="b=\\"', 'b=\\'),
++ (r'a="b=\="', 'b=='),
++ (r'a="b=\n"', 'b=n'),
++ (r'a="b=\042"', 'b="'),
++ (r'a="b=\134"', 'b=\\'),
++ (r'a="b=\377"', 'b=\xff'),
++ (r'a="b=\400"', 'b=400'),
++ (r'a="b=\42"', 'b=42'),
++ (r'a="b=\\042"', 'b=\\042'),
++ (r'a="b=\\134"', 'b=\\134'),
++ (r'a="b=\\\""', 'b=\\"'),
++ (r'a="b=\\\042"', 'b=\\"'),
++ (r'a="b=\134\""', 'b=\\"'),
++ (r'a="b=\134\042"', 'b=\\"'),
++ ]
++ for encoded, decoded in cases:
++ with self.subTest(encoded):
++ C = cookies.SimpleCookie()
++ C.load(encoded)
++ self.assertEqual(C['a'].value, decoded)
++
++ @support.requires_resource('cpu')
++ def test_unquote_large(self):
++ n = 10**6
++ for encoded in r'\\', r'\134':
++ with self.subTest(encoded):
++ data = 'a="b=' + encoded*n + ';"'
++ C = cookies.SimpleCookie()
++ C.load(data)
++ value = C['a'].value
++ self.assertEqual(value[:3], 'b=\\')
++ self.assertEqual(value[-2:], '\\;')
++ self.assertEqual(len(value), n + 3)
++
+ def test_load(self):
+ C = cookies.SimpleCookie()
+ C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme')
+--
+2.30.2
+
diff -Nru python3.11-3.11.2/debian/patches/0004-3.11-gh-124651-Quote-template-strings-in-venv-activa.patch python3.11-3.11.2/debian/patches/0004-3.11-gh-124651-Quote-template-strings-in-venv-activa.patch
--- python3.11-3.11.2/debian/patches/0004-3.11-gh-124651-Quote-template-strings-in-venv-activa.patch 1970-01-01 02:00:00.000000000 +0200
+++ python3.11-3.11.2/debian/patches/0004-3.11-gh-124651-Quote-template-strings-in-venv-activa.patch 2024-11-30 23:22:50.000000000 +0200
@@ -0,0 +1,281 @@
+From d991bc1fed3e1a038dfdeba2deb9d59cf9fe130c Mon Sep 17 00:00:00 2001
+From: Victor Stinner <vstinner@python.org>
+Date: Fri, 1 Nov 2024 14:11:47 +0100
+Subject: [3.11] gh-124651: Quote template strings in `venv` activation scripts
+ (GH-124712) (GH-126185) (#126269)
+
+---
+ Lib/test/test_venv.py | 83 +++++++++++++++++++++++++++-
+ Lib/venv/__init__.py | 42 ++++++++++++--
+ Lib/venv/scripts/common/activate | 8 +--
+ Lib/venv/scripts/posix/activate.csh | 8 +--
+ Lib/venv/scripts/posix/activate.fish | 8 +--
+ 5 files changed, 131 insertions(+), 18 deletions(-)
+
+diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
+index 86ce60fef13..3ec50e5b1cd 100644
+--- a/Lib/test/test_venv.py
++++ b/Lib/test/test_venv.py
+@@ -17,7 +17,8 @@
+ import sys
+ import sysconfig
+ import tempfile
+-from test.support import (captured_stdout, captured_stderr, requires_zlib,
++import shlex
++from test.support import (captured_stdout, captured_stderr,
+ skip_if_broken_multiprocessing_synchronize, verbose,
+ requires_subprocess, is_emscripten, is_wasi,
+ requires_venv_with_pip, TEST_HOME_DIR)
+@@ -95,6 +96,10 @@ def get_text_file_contents(self, *args, encoding='utf-8'):
+ result = f.read()
+ return result
+
++ def assertEndsWith(self, string, tail):
++ if not string.endswith(tail):
++ self.fail(f"String {string!r} does not end with {tail!r}")
++
+ class BasicTest(BaseTest):
+ """Test venv module functionality."""
+
+@@ -444,6 +449,82 @@ def test_executable_symlinks(self):
+ 'import sys; print(sys.executable)'])
+ self.assertEqual(out.strip(), envpy.encode())
+
++ # gh-124651: test quoted strings
++ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
++ def test_special_chars_bash(self):
++ """
++ Test that the template strings are quoted properly (bash)
++ """
++ rmtree(self.env_dir)
++ bash = shutil.which('bash')
++ if bash is None:
++ self.skipTest('bash required for this test')
++ env_name = '"\';&&$e|\'"'
++ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
++ builder = venv.EnvBuilder(clear=True)
++ builder.create(env_dir)
++ activate = os.path.join(env_dir, self.bindir, 'activate')
++ test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
++ with open(test_script, "w") as f:
++ f.write(f'source {shlex.quote(activate)}\n'
++ 'python -c \'import sys; print(sys.executable)\'\n'
++ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
++ 'deactivate\n')
++ out, err = check_output([bash, test_script])
++ lines = out.splitlines()
++ self.assertTrue(env_name.encode() in lines[0])
++ self.assertEndsWith(lines[1], env_name.encode())
++
++ # gh-124651: test quoted strings
++ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
++ def test_special_chars_csh(self):
++ """
++ Test that the template strings are quoted properly (csh)
++ """
++ rmtree(self.env_dir)
++ csh = shutil.which('tcsh') or shutil.which('csh')
++ if csh is None:
++ self.skipTest('csh required for this test')
++ env_name = '"\';&&$e|\'"'
++ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
++ builder = venv.EnvBuilder(clear=True)
++ builder.create(env_dir)
++ activate = os.path.join(env_dir, self.bindir, 'activate.csh')
++ test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
++ with open(test_script, "w") as f:
++ f.write(f'source {shlex.quote(activate)}\n'
++ 'python -c \'import sys; print(sys.executable)\'\n'
++ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
++ 'deactivate\n')
++ out, err = check_output([csh, test_script])
++ lines = out.splitlines()
++ self.assertTrue(env_name.encode() in lines[0])
++ self.assertEndsWith(lines[1], env_name.encode())
++
++ # gh-124651: test quoted strings on Windows
++ @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
++ def test_special_chars_windows(self):
++ """
++ Test that the template strings are quoted properly on Windows
++ """
++ rmtree(self.env_dir)
++ env_name = "'&&^$e"
++ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
++ builder = venv.EnvBuilder(clear=True)
++ builder.create(env_dir)
++ activate = os.path.join(env_dir, self.bindir, 'activate.bat')
++ test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
++ with open(test_batch, "w") as f:
++ f.write('@echo off\n'
++ f'"{activate}" & '
++ f'{self.exe} -c "import sys; print(sys.executable)" & '
++ f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
++ 'deactivate')
++ out, err = check_output([test_batch])
++ lines = out.splitlines()
++ self.assertTrue(env_name.encode() in lines[0])
++ self.assertEndsWith(lines[1], env_name.encode())
++
+ @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
+ def test_unicode_in_batch_file(self):
+ """
+diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py
+index d487fa75ad3..b416e23c2f0 100644
+--- a/Lib/venv/__init__.py
++++ b/Lib/venv/__init__.py
+@@ -11,6 +11,7 @@
+ import sys
+ import sysconfig
+ import types
++import shlex
+
+
+ CORE_VENV_DEPS = ('pip', 'setuptools')
+@@ -412,11 +413,41 @@ def replace_variables(self, text, context):
+ :param context: The information for the environment creation request
+ being processed.
+ """
+- text = text.replace('__VENV_DIR__', context.env_dir)
+- text = text.replace('__VENV_NAME__', context.env_name)
+- text = text.replace('__VENV_PROMPT__', context.prompt)
+- text = text.replace('__VENV_BIN_NAME__', context.bin_name)
+- text = text.replace('__VENV_PYTHON__', context.env_exe)
++ replacements = {
++ '__VENV_DIR__': context.env_dir,
++ '__VENV_NAME__': context.env_name,
++ '__VENV_PROMPT__': context.prompt,
++ '__VENV_BIN_NAME__': context.bin_name,
++ '__VENV_PYTHON__': context.env_exe,
++ }
++
++ def quote_ps1(s):
++ """
++ This should satisfy PowerShell quoting rules [1], unless the quoted
++ string is passed directly to Windows native commands [2].
++ [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
++ [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
++ """
++ s = s.replace("'", "''")
++ return f"'{s}'"
++
++ def quote_bat(s):
++ return s
++
++ # gh-124651: need to quote the template strings properly
++ quote = shlex.quote
++ script_path = context.script_path
++ if script_path.endswith('.ps1'):
++ quote = quote_ps1
++ elif script_path.endswith('.bat'):
++ quote = quote_bat
++ else:
++ # fallbacks to POSIX shell compliant quote
++ quote = shlex.quote
++
++ replacements = {key: quote(s) for key, s in replacements.items()}
++ for key, quoted in replacements.items():
++ text = text.replace(key, quoted)
+ return text
+
+ def install_scripts(self, context, path):
+@@ -456,6 +487,7 @@ def install_scripts(self, context, path):
+ with open(srcfile, 'rb') as f:
+ data = f.read()
+ if not srcfile.endswith(('.exe', '.pdb')):
++ context.script_path = srcfile
+ try:
+ data = data.decode('utf-8')
+ data = self.replace_variables(data, context)
+diff --git a/Lib/venv/scripts/common/activate b/Lib/venv/scripts/common/activate
+index 6fbc2b8801d..104399d55f8 100644
+--- a/Lib/venv/scripts/common/activate
++++ b/Lib/venv/scripts/common/activate
+@@ -38,11 +38,11 @@ deactivate () {
+ # unset irrelevant variables
+ deactivate nondestructive
+
+-VIRTUAL_ENV="__VENV_DIR__"
++VIRTUAL_ENV=__VENV_DIR__
+ export VIRTUAL_ENV
+
+ _OLD_VIRTUAL_PATH="$PATH"
+-PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
++PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
+ export PATH
+
+ # unset PYTHONHOME if set
+@@ -55,9 +55,9 @@ fi
+
+ if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+ _OLD_VIRTUAL_PS1="${PS1:-}"
+- PS1="__VENV_PROMPT__${PS1:-}"
++ PS1=__VENV_PROMPT__"${PS1:-}"
+ export PS1
+- VIRTUAL_ENV_PROMPT="__VENV_PROMPT__"
++ VIRTUAL_ENV_PROMPT=__VENV_PROMPT__
+ export VIRTUAL_ENV_PROMPT
+ fi
+
+diff --git a/Lib/venv/scripts/posix/activate.csh b/Lib/venv/scripts/posix/activate.csh
+index d6f697c55ed..c47702127ef 100644
+--- a/Lib/venv/scripts/posix/activate.csh
++++ b/Lib/venv/scripts/posix/activate.csh
+@@ -8,17 +8,17 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
+ # Unset irrelevant variables.
+ deactivate nondestructive
+
+-setenv VIRTUAL_ENV "__VENV_DIR__"
++setenv VIRTUAL_ENV __VENV_DIR__
+
+ set _OLD_VIRTUAL_PATH="$PATH"
+-setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
++setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
+
+
+ set _OLD_VIRTUAL_PROMPT="$prompt"
+
+ if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
+- set prompt = "__VENV_PROMPT__$prompt"
+- setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
++ set prompt = __VENV_PROMPT__"$prompt"
++ setenv VIRTUAL_ENV_PROMPT __VENV_PROMPT__
+ endif
+
+ alias pydoc python -m pydoc
+diff --git a/Lib/venv/scripts/posix/activate.fish b/Lib/venv/scripts/posix/activate.fish
+index 9aa4446005f..dc3a6c88270 100644
+--- a/Lib/venv/scripts/posix/activate.fish
++++ b/Lib/venv/scripts/posix/activate.fish
+@@ -33,10 +33,10 @@ end
+ # Unset irrelevant variables.
+ deactivate nondestructive
+
+-set -gx VIRTUAL_ENV "__VENV_DIR__"
++set -gx VIRTUAL_ENV __VENV_DIR__
+
+ set -gx _OLD_VIRTUAL_PATH $PATH
+-set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
++set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
+
+ # Unset PYTHONHOME if set.
+ if set -q PYTHONHOME
+@@ -56,7 +56,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+ set -l old_status $status
+
+ # Output the venv prompt; color taken from the blue of the Python logo.
+- printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal)
++ printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
+
+ # Restore the return status of the previous command.
+ echo "exit $old_status" | .
+@@ -65,5 +65,5 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+ end
+
+ set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
+- set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
++ set -gx VIRTUAL_ENV_PROMPT __VENV_PROMPT__
+ end
+--
+2.30.2
+
diff -Nru python3.11-3.11.2/debian/patches/0005-3.11-gh-103848-Adds-checks-to-ensure-that-bracketed-.patch python3.11-3.11.2/debian/patches/0005-3.11-gh-103848-Adds-checks-to-ensure-that-bracketed-.patch
--- python3.11-3.11.2/debian/patches/0005-3.11-gh-103848-Adds-checks-to-ensure-that-bracketed-.patch 1970-01-01 02:00:00.000000000 +0200
+++ python3.11-3.11.2/debian/patches/0005-3.11-gh-103848-Adds-checks-to-ensure-that-bracketed-.patch 2024-11-30 23:22:50.000000000 +0200
@@ -0,0 +1,108 @@
+From 761ccd306d0eeba0ad0f91878ed031b6d54cc1b9 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Tue, 9 May 2023 23:35:24 -0700
+Subject: [3.11] gh-103848: Adds checks to ensure that bracketed hosts found by
+ urlsplit are of IPv6 or IPvFuture format (GH-103849) (#104349)
+
+gh-103848: Adds checks to ensure that bracketed hosts found by urlsplit are of IPv6 or IPvFuture format (GH-103849)
+
+* Adds checks to ensure that bracketed hosts found by urlsplit are of IPv6 or IPvFuture format
+
+---------
+
+(cherry picked from commit 29f348e232e82938ba2165843c448c2b291504c5)
+
+Co-authored-by: JohnJamesUtley <81572567+JohnJamesUtley@users.noreply.github.com>
+Co-authored-by: Gregory P. Smith <greg@krypto.org>
+---
+ Lib/test/test_urlparse.py | 26 ++++++++++++++++++++++++++
+ Lib/urllib/parse.py | 16 +++++++++++++++-
+ 2 files changed, 41 insertions(+), 1 deletion(-)
+
+diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py
+index 40f13d631cd..83ea618291e 100644
+--- a/Lib/test/test_urlparse.py
++++ b/Lib/test/test_urlparse.py
+@@ -1092,6 +1092,32 @@ def test_issue14072(self):
+ self.assertEqual(p2.scheme, 'tel')
+ self.assertEqual(p2.path, '+31641044153')
+
++ def test_invalid_bracketed_hosts(self):
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[192.0.2.146]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[important.com:8000]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v123r.IP]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v12ae]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v.IP]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v123.]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af::2309::fae7:1234]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af:2309::fae7:1234:2342:438e:192.0.2.146]/Path?Query')
++ self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@]v6a.ip[/Path')
++
++ def test_splitting_bracketed_hosts(self):
++ p1 = urllib.parse.urlsplit('scheme://user@[v6a.ip]/path?query')
++ self.assertEqual(p1.hostname, 'v6a.ip')
++ self.assertEqual(p1.username, 'user')
++ self.assertEqual(p1.path, '/path')
++ p2 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7%test]/path?query')
++ self.assertEqual(p2.hostname, '0439:23af:2309::fae7%test')
++ self.assertEqual(p2.username, 'user')
++ self.assertEqual(p2.path, '/path')
++ p3 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7:1234:192.0.2.146%test]/path?query')
++ self.assertEqual(p3.hostname, '0439:23af:2309::fae7:1234:192.0.2.146%test')
++ self.assertEqual(p3.username, 'user')
++ self.assertEqual(p3.path, '/path')
++
+ def test_port_casting_failure_message(self):
+ message = "Port could not be cast to integer value as 'oracle'"
+ p1 = urllib.parse.urlparse('http://Server=sde; Service=sde:oracle')
+diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py
+index 4f06fd509e6..e5f0b784bf6 100644
+--- a/Lib/urllib/parse.py
++++ b/Lib/urllib/parse.py
+@@ -37,6 +37,7 @@
+ import sys
+ import types
+ import warnings
++import ipaddress
+
+ __all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
+ "urlsplit", "urlunsplit", "urlencode", "parse_qs",
+@@ -435,6 +436,17 @@ def _checknetloc(netloc):
+ raise ValueError("netloc '" + netloc + "' contains invalid " +
+ "characters under NFKC normalization")
+
++# Valid bracketed hosts are defined in
++# https://www.rfc-editor.org/rfc/rfc3986#page-49 and https://url.spec.whatwg.org/
++def _check_bracketed_host(hostname):
++ if hostname.startswith('v'):
++ if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", hostname):
++ raise ValueError(f"IPvFuture address is invalid")
++ else:
++ ip = ipaddress.ip_address(hostname) # Throws Value Error if not IPv6 or IPv4
++ if isinstance(ip, ipaddress.IPv4Address):
++ raise ValueError(f"An IPv4 address cannot be in brackets")
++
+ # typed=True avoids BytesWarnings being emitted during cache key
+ # comparison since this API supports both bytes and str input.
+ @functools.lru_cache(typed=True)
+@@ -478,12 +490,14 @@ def urlsplit(url, scheme='', allow_fragments=True):
+ break
+ else:
+ scheme, url = url[:i].lower(), url[i+1:]
+-
+ if url[:2] == '//':
+ netloc, url = _splitnetloc(url, 2)
+ if (('[' in netloc and ']' not in netloc) or
+ (']' in netloc and '[' not in netloc)):
+ raise ValueError("Invalid IPv6 URL")
++ if '[' in netloc and ']' in netloc:
++ bracketed_host = netloc.partition('[')[2].partition(']')[0]
++ _check_bracketed_host(bracketed_host)
+ if allow_fragments and '#' in url:
+ url, fragment = url.split('#', 1)
+ if '?' in url:
+--
+2.30.2
+
diff -Nru python3.11-3.11.2/debian/patches/series python3.11-3.11.2/debian/patches/series
--- python3.11-3.11.2/debian/patches/series 2024-09-14 05:57:20.000000000 +0300
+++ python3.11-3.11.2/debian/patches/series 2024-11-30 23:22:50.000000000 +0200
@@ -53,3 +53,8 @@
CVE-2024-8088.diff
0001-3.11-gh-123270-Replaced-SanitizedNames-with-a-more-s.patch
CVE-2024-6232.patch
+0001-3.11-CVE-2023-27043-gh-102988-Reject-malformed-addre.patch
+0002-3.11-gh-121650-Encode-newlines-in-headers-and-verify.patch
+0003-3.11-gh-123067-Fix-quadratic-complexity-in-parsing-q.patch
+0004-3.11-gh-124651-Quote-template-strings-in-venv-activa.patch
+0005-3.11-gh-103848-Adds-checks-to-ensure-that-bracketed-.patch
Reply to: