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