Package: release.debian.org
Severity: normal
Tags: bookworm moreinfo
User: release.debian....@packages.debian.org
Usertags: pu
X-Debbugs-Cc: secur...@debian.org, Matthias Klose <d...@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 <b...@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 <encu...@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 <vstin...@python.org>
+Co-authored-by: Thomas Dwyer <git...@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(['al...@example.com <b...@example.com>'])
++    # Invalid output: [('', 'al...@example.com'), ('', 'b...@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', 'aper...@dom.ain'),
+             ('Bud Person', 'bper...@dom.ain')])
+ 
++    def test_getaddresses_comma_in_name(self):
++        """GH-106669 regression test."""
++        self.assertEqual(
++            utils.getaddresses(
++                [
++                    '"Bud, Person" <bper...@dom.ain>',
++                    'aper...@dom.ain (Al Person)',
++                    '"Mariusz Felisiak" <t...@example.com>',
++                ]
++            ),
++            [
++                ('Bud, Person', 'bper...@dom.ain'),
++                ('Al Person', 'aper...@dom.ain'),
++                ('Mariusz Felisiak', 't...@example.com'),
++            ],
++        )
++
++    def test_parsing_errors(self):
++        """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
++        alice = 'al...@example.org'
++        bob = 'b...@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, al...@example.org" <b...@example.com>'
++        expected_strict = ('Alice, al...@example.org', 'b...@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 = 'al...@example.org (Alice)'
++        expected_strict = ('Alice', 'al...@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 = 'al...@example.org )Alice('
++        self.assertEqual(utils.getaddresses([address]), [empty])
++        self.assertEqual(utils.getaddresses([address], strict=False),
++                         [('', 'al...@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" <j...@example.net>, "John Doe" 
<j...@example.net>'
++        self.assertEqual(utils.getaddresses([address]),
++                         [('Jane Doe', 'j...@example.net'),
++                          ('John Doe', 'j...@example.net')])
++        self.assertEqual(utils.getaddresses([address], strict=False),
++                         [('Jane Doe', 'j...@example.net'),
++                          ('John Doe', 'j...@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" <ja...@dom.ain>']),
+-           [('', ''), ('Jason R. Mastaler', 'ja...@dom.ain')])
++        for addresses, expected in (
++            (['"Sürname, Firstname" <t...@example.com>'],
++             [('Sürname, Firstname', 't...@example.com')]),
++
++            (['foo: ;'],
++             [('', '')]),
++
++            (['foo: ;', '"Jason R. Mastaler" <ja...@dom.ain>'],
++             [('', ''), ('Jason R. Mastaler', 'ja...@dom.ain')]),
++
++            ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his 
host)>'],
++             [('Pete (A nice ) chap his account his host)', 
'p...@silly.test')]),
++
++            (['(Empty list)(start)Undisclosed recipients  :(nobody(I know))'],
++             [('', '')]),
++
++            (['Mary <@machine.tld:m...@example.net>, , jdoe@test   . 
example'],
++             [('Mary', 'm...@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'<b...@nil.test>, "Giant; \"Big\" Box" <b...@example.net>'],
++             [('', 'b...@nil.test'), ('Giant; "Big" Box', 
'b...@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" <j...@example.net>, "John Doe" <j...@example.net>',
++              ' <j...@example.net>,  <j...@example.net>')
++        check(r'"Jane \"Doe\"." <j...@example.net>',
++              ' <j...@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 <j...@example.net>, John Doe <j...@example.net>',
++            'lone " quote',
++        ):
++            self.assertEqual(utils._strip_quoted_realnames(addr), addr)
++
++
++    def test_check_parenthesis(self):
++        addr = 'al...@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?= <luk...@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 <encu...@gmail.com>
+Co-authored-by: Bas Bloemsaat <b...@bloemsaat.org>
+Co-authored-by: Serhiy Storchaka <storch...@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: inject...@example.com
++
++            None
++            """)))
++        expected = textwrap.dedent("""\
++            To: nobody
++            Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: inject...@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: inject...@example.com
++
++            None
++            """)))
++        expected = textwrap.dedent("""\
++            To: nobody
++            Subject: Bad subject
++             =?utf-8?q?=0A?=Bcc:
++             inject...@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-isling...@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 <storch...@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 <vstin...@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-isling...@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+johnjamesut...@users.noreply.github.com>
+Co-authored-by: Gregory P. Smith <g...@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 via email to