[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1089071: marked as done (bookworm-pu: package python3.11/3.11.2-6+deb12u5)



Your message dated Sat, 11 Jan 2025 11:03:09 +0000
with message-id <E1tWZGn-009jaV-Bw@coccia.debian.org>
and subject line Close 1089071
has caused the Debian Bug report #1089071,
regarding bookworm-pu: package python3.11/3.11.2-6+deb12u5
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
1089071: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1089071
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
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

--- End Message ---
--- Begin Message ---
Version: 12.9
This update has been released as part of 12.9. Thank you for your contribution.

--- End Message ---

Reply to: