Accepting request 1201476 from devel:languages:python:Factory
Automatic submission by obs-autosubmit OBS-URL: https://build.opensuse.org/request/show/1201476 OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python38?expand=0&rev=54
This commit is contained in:
commit
387acaaa01
@ -1,462 +0,0 @@
|
|||||||
---
|
|
||||||
Doc/library/email.utils.rst | 19 -
|
|
||||||
Lib/email/utils.py | 151 +++++++-
|
|
||||||
Lib/test/test_email/test_email.py | 187 +++++++++-
|
|
||||||
Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8
|
|
||||||
4 files changed, 344 insertions(+), 21 deletions(-)
|
|
||||||
|
|
||||||
--- a/Doc/library/email.utils.rst
|
|
||||||
+++ b/Doc/library/email.utils.rst
|
|
||||||
@@ -60,13 +60,18 @@ of the new API.
|
|
||||||
begins with angle brackets, they are stripped off.
|
|
||||||
|
|
||||||
|
|
||||||
-.. function:: parseaddr(address)
|
|
||||||
+.. function:: parseaddr(address, *, strict=True)
|
|
||||||
|
|
||||||
Parse address -- which should be the value of some address-containing field such
|
|
||||||
as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
|
|
||||||
*email address* parts. Returns a tuple of that information, unless the parse
|
|
||||||
fails, in which case a 2-tuple of ``('', '')`` is returned.
|
|
||||||
|
|
||||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
|
||||||
+
|
|
||||||
+ .. versionchanged:: 3.13
|
|
||||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
|
||||||
+
|
|
||||||
|
|
||||||
.. function:: formataddr(pair, charset='utf-8')
|
|
||||||
|
|
||||||
@@ -84,12 +89,15 @@ of the new API.
|
|
||||||
Added the *charset* option.
|
|
||||||
|
|
||||||
|
|
||||||
-.. function:: getaddresses(fieldvalues)
|
|
||||||
+.. function:: getaddresses(fieldvalues, *, strict=True)
|
|
||||||
|
|
||||||
This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
|
|
||||||
*fieldvalues* is a sequence of header field values as might be returned by
|
|
||||||
- :meth:`Message.get_all <email.message.Message.get_all>`. Here's a simple
|
|
||||||
- example that gets all the recipients of a message::
|
|
||||||
+ :meth:`Message.get_all <email.message.Message.get_all>`.
|
|
||||||
+
|
|
||||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
|
||||||
+
|
|
||||||
+ Here's a simple example that gets all the recipients of a message::
|
|
||||||
|
|
||||||
from email.utils import getaddresses
|
|
||||||
|
|
||||||
@@ -99,6 +107,9 @@ of the new API.
|
|
||||||
resent_ccs = msg.get_all('resent-cc', [])
|
|
||||||
all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
|
|
||||||
|
|
||||||
+ .. versionchanged:: 3.13
|
|
||||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
|
||||||
+
|
|
||||||
|
|
||||||
.. function:: parsedate(date)
|
|
||||||
|
|
||||||
--- a/Lib/email/utils.py
|
|
||||||
+++ b/Lib/email/utils.py
|
|
||||||
@@ -48,6 +48,7 @@ TICK = "'"
|
|
||||||
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
|
||||||
escapesre = re.compile(r'[\\"]')
|
|
||||||
|
|
||||||
+
|
|
||||||
def _has_surrogates(s):
|
|
||||||
"""Return True if s contains surrogate-escaped binary data."""
|
|
||||||
# This check is based on the fact that unless there are surrogates, utf8
|
|
||||||
@@ -105,13 +106,128 @@ def formataddr(pair, charset='utf-8'):
|
|
||||||
return '%s%s%s <%s>' % (quotes, name, quotes, address)
|
|
||||||
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.
|
|
||||||
|
|
||||||
-def getaddresses(fieldvalues):
|
|
||||||
- """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
|
|
||||||
- all = COMMASPACE.join(fieldvalues)
|
|
||||||
- a = _AddressList(all)
|
|
||||||
- return a.addresslist
|
|
||||||
+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
|
|
||||||
+ its place.
|
|
||||||
+
|
|
||||||
+ If strict is true, use a strict parser which rejects malformed inputs.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ # If strict is true, if the resulting list of parsed addresses is greater
|
|
||||||
+ # than the number of fieldvalues in the input list, a parsing error has
|
|
||||||
+ # occurred and consequently a list containing a single empty 2-tuple [('',
|
|
||||||
+ # '')] is returned in its place. This is done to avoid invalid output.
|
|
||||||
+ #
|
|
||||||
+ # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
|
|
||||||
+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
|
|
||||||
+ # Safe output: [('', '')]
|
|
||||||
+
|
|
||||||
+ if not strict:
|
|
||||||
+ all = COMMASPACE.join(str(v) for v in fieldvalues)
|
|
||||||
+ a = _AddressList(all)
|
|
||||||
+ return a.addresslist
|
|
||||||
+
|
|
||||||
+ fieldvalues = [str(v) for v in fieldvalues]
|
|
||||||
+ fieldvalues = _pre_parse_validation(fieldvalues)
|
|
||||||
+ addr = COMMASPACE.join(fieldvalues)
|
|
||||||
+ a = _AddressList(addr)
|
|
||||||
+ result = _post_parse_validation(a.addresslist)
|
|
||||||
+
|
|
||||||
+ # Treat output as invalid if the number of addresses is not equal to the
|
|
||||||
+ # expected number of addresses.
|
|
||||||
+ n = 0
|
|
||||||
+ for v in fieldvalues:
|
|
||||||
+ # When a comma is used in the Real Name part it is not a deliminator.
|
|
||||||
+ # So strip those out before counting the commas.
|
|
||||||
+ v = _strip_quoted_realnames(v)
|
|
||||||
+ # Expected number of addresses: 1 + number of commas
|
|
||||||
+ n += 1 + v.count(',')
|
|
||||||
+ if len(result) != n:
|
|
||||||
+ return [('', '')]
|
|
||||||
+
|
|
||||||
+ return result
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _check_parenthesis(addr):
|
|
||||||
+ # Ignore parenthesis in quoted real names.
|
|
||||||
+ addr = _strip_quoted_realnames(addr)
|
|
||||||
+
|
|
||||||
+ opens = 0
|
|
||||||
+ for pos, ch in _iter_escaped_chars(addr):
|
|
||||||
+ if ch == '(':
|
|
||||||
+ opens += 1
|
|
||||||
+ elif ch == ')':
|
|
||||||
+ opens -= 1
|
|
||||||
+ if opens < 0:
|
|
||||||
+ return False
|
|
||||||
+ return (opens == 0)
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _pre_parse_validation(email_header_fields):
|
|
||||||
+ accepted_values = []
|
|
||||||
+ for v in email_header_fields:
|
|
||||||
+ if not _check_parenthesis(v):
|
|
||||||
+ v = "('', '')"
|
|
||||||
+ accepted_values.append(v)
|
|
||||||
+
|
|
||||||
+ return accepted_values
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _post_parse_validation(parsed_email_header_tuples):
|
|
||||||
+ accepted_values = []
|
|
||||||
+ # The parser would have parsed a correctly formatted domain-literal
|
|
||||||
+ # The existence of an [ after parsing indicates a parsing failure
|
|
||||||
+ for v in parsed_email_header_tuples:
|
|
||||||
+ if '[' in v[1]:
|
|
||||||
+ v = ('', '')
|
|
||||||
+ accepted_values.append(v)
|
|
||||||
+
|
|
||||||
+ return accepted_values
|
|
||||||
|
|
||||||
|
|
||||||
def _format_timetuple_and_zone(timetuple, zone):
|
|
||||||
@@ -202,16 +318,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]
|
|
||||||
|
|
||||||
|
|
||||||
--- a/Lib/test/test_email/test_email.py
|
|
||||||
+++ b/Lib/test/test_email/test_email.py
|
|
||||||
@@ -16,6 +16,7 @@ from unittest.mock import patch
|
|
||||||
|
|
||||||
import email
|
|
||||||
import email.policy
|
|
||||||
+import email.utils
|
|
||||||
|
|
||||||
from email.charset import Charset
|
|
||||||
from email.header import Header, decode_header, make_header
|
|
||||||
@@ -3248,15 +3249,137 @@ Foo
|
|
||||||
[('Al Person', 'aperson@dom.ain'),
|
|
||||||
('Bud Person', 'bperson@dom.ain')])
|
|
||||||
|
|
||||||
+ def test_parsing_errors(self):
|
|
||||||
+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
|
|
||||||
+ alice = 'alice@example.org'
|
|
||||||
+ bob = 'bob@example.com'
|
|
||||||
+ empty = ('', '')
|
|
||||||
+
|
|
||||||
+ # Test utils.getaddresses() and utils.parseaddr() on malformed email
|
|
||||||
+ # addresses: default behavior (strict=True) rejects malformed address,
|
|
||||||
+ # and strict=False which tolerates malformed address.
|
|
||||||
+ for invalid_separator, expected_non_strict in (
|
|
||||||
+ ('(', [(f'<{bob}>', alice)]),
|
|
||||||
+ (')', [('', alice), empty, ('', bob)]),
|
|
||||||
+ ('<', [('', alice), empty, ('', bob), empty]),
|
|
||||||
+ ('>', [('', alice), empty, ('', bob)]),
|
|
||||||
+ ('[', [('', f'{alice}[<{bob}>]')]),
|
|
||||||
+ (']', [('', alice), empty, ('', bob)]),
|
|
||||||
+ ('@', [empty, empty, ('', bob)]),
|
|
||||||
+ (';', [('', alice), empty, ('', bob)]),
|
|
||||||
+ (':', [('', alice), ('', bob)]),
|
|
||||||
+ ('.', [('', alice + '.'), ('', bob)]),
|
|
||||||
+ ('"', [('', alice), ('', f'<{bob}>')]),
|
|
||||||
+ ):
|
|
||||||
+ address = f'{alice}{invalid_separator}<{bob}>'
|
|
||||||
+ with self.subTest(address=address):
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [empty])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
|
||||||
+ expected_non_strict)
|
|
||||||
+
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]),
|
|
||||||
+ empty)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Comma (',') is treated differently depending on strict parameter.
|
|
||||||
+ # Comma without quotes.
|
|
||||||
+ address = f'{alice},<{bob}>'
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [('', alice), ('', bob)])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
|
||||||
+ [('', alice), ('', bob)])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]),
|
|
||||||
+ empty)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Real name between quotes containing comma.
|
|
||||||
+ address = '"Alice, alice@example.org" <bob@example.com>'
|
|
||||||
+ expected_strict = ('Alice, alice@example.org', 'bob@example.com')
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Valid parenthesis in comments.
|
|
||||||
+ address = 'alice@example.org (Alice)'
|
|
||||||
+ expected_strict = ('Alice', 'alice@example.org')
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Invalid parenthesis in comments.
|
|
||||||
+ address = 'alice@example.org )Alice('
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]), [empty])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
|
||||||
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Two addresses with quotes separated by comma.
|
|
||||||
+ address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
|
|
||||||
+ self.assertEqual(utils.getaddresses([address]),
|
|
||||||
+ [('Jane Doe', 'jane@example.net'),
|
|
||||||
+ ('John Doe', 'john@example.net')])
|
|
||||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
|
||||||
+ [('Jane Doe', 'jane@example.net'),
|
|
||||||
+ ('John Doe', 'john@example.net')])
|
|
||||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
|
||||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
|
||||||
+ ('', address))
|
|
||||||
+
|
|
||||||
+ # Test email.utils.supports_strict_parsing attribute
|
|
||||||
+ self.assertEqual(email.utils.supports_strict_parsing, True)
|
|
||||||
+
|
|
||||||
def test_getaddresses_nasty(self):
|
|
||||||
- eq = self.assertEqual
|
|
||||||
- eq(utils.getaddresses(['foo: ;']), [('', '')])
|
|
||||||
- eq(utils.getaddresses(
|
|
||||||
- ['[]*-- =~$']),
|
|
||||||
- [('', ''), ('', ''), ('', '*--')])
|
|
||||||
- eq(utils.getaddresses(
|
|
||||||
- ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
|
|
||||||
- [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
|
|
||||||
+ for addresses, expected in (
|
|
||||||
+ (['"Sürname, Firstname" <to@example.com>'],
|
|
||||||
+ [('Sürname, Firstname', 'to@example.com')]),
|
|
||||||
+
|
|
||||||
+ (['foo: ;'],
|
|
||||||
+ [('', '')]),
|
|
||||||
+
|
|
||||||
+ (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
|
|
||||||
+ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
|
|
||||||
+
|
|
||||||
+ ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
|
|
||||||
+ [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
|
|
||||||
+
|
|
||||||
+ (['(Empty list)(start)Undisclosed recipients :(nobody(I know))'],
|
|
||||||
+ [('', '')]),
|
|
||||||
+
|
|
||||||
+ (['Mary <@machine.tld:mary@example.net>, , jdoe@test . example'],
|
|
||||||
+ [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
|
|
||||||
+
|
|
||||||
+ (['John Doe <jdoe@machine(comment). example>'],
|
|
||||||
+ [('John Doe (comment)', 'jdoe@machine.example')]),
|
|
||||||
+
|
|
||||||
+ (['"Mary Smith: Personal Account" <smith@home.example>'],
|
|
||||||
+ [('Mary Smith: Personal Account', 'smith@home.example')]),
|
|
||||||
+
|
|
||||||
+ (['Undisclosed recipients:;'],
|
|
||||||
+ [('', '')]),
|
|
||||||
+
|
|
||||||
+ ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
|
|
||||||
+ [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
|
|
||||||
+ ):
|
|
||||||
+ with self.subTest(addresses=addresses):
|
|
||||||
+ self.assertEqual(utils.getaddresses(addresses),
|
|
||||||
+ expected)
|
|
||||||
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
|
|
||||||
+ expected)
|
|
||||||
+
|
|
||||||
+ addresses = ['[]*-- =~$']
|
|
||||||
+ self.assertEqual(utils.getaddresses(addresses),
|
|
||||||
+ [('', '')])
|
|
||||||
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
|
|
||||||
+ [('', ''), ('', ''), ('', '*--')])
|
|
||||||
|
|
||||||
def test_getaddresses_embedded_comment(self):
|
|
||||||
"""Test proper handling of a nested comment"""
|
|
||||||
@@ -3440,6 +3563,54 @@ multipart/report
|
|
||||||
m = cls(*constructor, policy=email.policy.default)
|
|
||||||
self.assertIs(m.policy, email.policy.default)
|
|
||||||
|
|
||||||
+ def test_iter_escaped_chars(self):
|
|
||||||
+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
|
|
||||||
+ [(0, 'a'),
|
|
||||||
+ (2, '\\\\'),
|
|
||||||
+ (3, 'b'),
|
|
||||||
+ (5, '\\"'),
|
|
||||||
+ (6, 'c'),
|
|
||||||
+ (8, '\\\\'),
|
|
||||||
+ (9, '"'),
|
|
||||||
+ (10, 'd')])
|
|
||||||
+ self.assertEqual(list(utils._iter_escaped_chars('a\\')),
|
|
||||||
+ [(0, 'a'), (1, '\\')])
|
|
||||||
+
|
|
||||||
+ def test_strip_quoted_realnames(self):
|
|
||||||
+ def check(addr, expected):
|
|
||||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), expected)
|
|
||||||
+
|
|
||||||
+ check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
|
|
||||||
+ ' <jane@example.net>, <john@example.net>')
|
|
||||||
+ check(r'"Jane \"Doe\"." <jane@example.net>',
|
|
||||||
+ ' <jane@example.net>')
|
|
||||||
+
|
|
||||||
+ # special cases
|
|
||||||
+ check(r'before"name"after', 'beforeafter')
|
|
||||||
+ check(r'before"name"', 'before')
|
|
||||||
+ check(r'b"name"', 'b') # single char
|
|
||||||
+ check(r'"name"after', 'after')
|
|
||||||
+ check(r'"name"a', 'a') # single char
|
|
||||||
+ check(r'"name"', '')
|
|
||||||
+
|
|
||||||
+ # no change
|
|
||||||
+ for addr in (
|
|
||||||
+ 'Jane Doe <jane@example.net>, John Doe <john@example.net>',
|
|
||||||
+ 'lone " quote',
|
|
||||||
+ ):
|
|
||||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), addr)
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_check_parenthesis(self):
|
|
||||||
+ addr = 'alice@example.net'
|
|
||||||
+ self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
|
|
||||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
|
|
||||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
|
|
||||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
|
|
||||||
+
|
|
||||||
+ # Ignore real name between quotes
|
|
||||||
+ self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
|
|
||||||
+
|
|
||||||
|
|
||||||
# Test the iterator/generators
|
|
||||||
class TestIterators(TestEmailBase):
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
|
||||||
@@ -0,0 +1,8 @@
|
|
||||||
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
|
|
||||||
+return ``('', '')`` 2-tuples in more situations where invalid email
|
|
||||||
+addresses are encountered instead of potentially inaccurate values. Add
|
|
||||||
+optional *strict* parameter to these two functions: use ``strict=False`` to
|
|
||||||
+get the old behavior, accept malformed inputs.
|
|
||||||
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
|
|
||||||
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
|
|
||||||
+Stinner to improve the CVE-2023-27043 fix.
|
|
@ -1,173 +0,0 @@
|
|||||||
From 732c7d512e7cdf656a3f02a38c329b14a14a8573 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Seth Michael Larson <seth@python.org>
|
|
||||||
Date: Fri, 19 Apr 2024 11:21:40 -0700
|
|
||||||
Subject: [PATCH] [3.9] gh-114572: Fix locking in cert_store_stats and
|
|
||||||
get_ca_certs
|
|
||||||
|
|
||||||
---
|
|
||||||
Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst | 4
|
|
||||||
Modules/_ssl.c | 91 +++++++++-
|
|
||||||
2 files changed, 92 insertions(+), 3 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst
|
|
||||||
|
|
||||||
Index: Python-3.8.19/Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst
|
|
||||||
===================================================================
|
|
||||||
--- /dev/null
|
|
||||||
+++ Python-3.8.19/Misc/NEWS.d/next/Security/2024-04-19-11-21-13.gh-issue-114572.t1QMQD.rst
|
|
||||||
@@ -0,0 +1,4 @@
|
|
||||||
+:meth:`ssl.SSLContext.cert_store_stats` and
|
|
||||||
+:meth:`ssl.SSLContext.get_ca_certs` now correctly lock access to the
|
|
||||||
+certificate store, when the :class:`ssl.SSLContext` is shared across
|
|
||||||
+multiple threads.
|
|
||||||
Index: Python-3.8.19/Modules/_ssl.c
|
|
||||||
===================================================================
|
|
||||||
--- Python-3.8.19.orig/Modules/_ssl.c
|
|
||||||
+++ Python-3.8.19/Modules/_ssl.c
|
|
||||||
@@ -168,6 +168,10 @@ extern const SSL_METHOD *TLSv1_2_method(
|
|
||||||
# define PY_OPENSSL_1_1_API 1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
+#if (OPENSSL_VERSION_NUMBER >= 0x30300000L) && !defined(LIBRESSL_VERSION_NUMBER)
|
|
||||||
+# define OPENSSL_VERSION_3_3 1
|
|
||||||
+#endif
|
|
||||||
+
|
|
||||||
/* SNI support (client- and server-side) appeared in OpenSSL 1.0.0 and 0.9.8f
|
|
||||||
* This includes the SSL_set_SSL_CTX() function.
|
|
||||||
*/
|
|
||||||
@@ -212,6 +216,16 @@ extern const SSL_METHOD *TLSv1_2_method(
|
|
||||||
#define HAVE_OPENSSL_CRYPTO_LOCK
|
|
||||||
#endif
|
|
||||||
|
|
||||||
+/* OpenSSL 1.1+ allows locking X509_STORE, 1.0.2 doesn't. */
|
|
||||||
+#ifdef OPENSSL_VERSION_1_1
|
|
||||||
+#define HAVE_OPENSSL_X509_STORE_LOCK
|
|
||||||
+#endif
|
|
||||||
+
|
|
||||||
+/* OpenSSL 3.3 added the X509_STORE_get1_objects API */
|
|
||||||
+#ifdef OPENSSL_VERSION_3_3
|
|
||||||
+#define HAVE_OPENSSL_X509_STORE_GET1_OBJECTS 1
|
|
||||||
+#endif
|
|
||||||
+
|
|
||||||
#if defined(OPENSSL_VERSION_1_1) && !defined(OPENSSL_NO_SSL2)
|
|
||||||
#define OPENSSL_NO_SSL2
|
|
||||||
#endif
|
|
||||||
@@ -4678,6 +4692,54 @@ set_sni_callback(PySSLContext *self, PyO
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
+/* Shim of X509_STORE_get1_objects API from OpenSSL 3.3
|
|
||||||
+ * Only available with the X509_STORE_lock() API */
|
|
||||||
+#if defined(HAVE_OPENSSL_X509_STORE_LOCK) && !defined(OPENSSL_VERSION_3_3)
|
|
||||||
+#define HAVE_OPENSSL_X509_STORE_GET1_OBJECTS 1
|
|
||||||
+
|
|
||||||
+static X509_OBJECT *x509_object_dup(const X509_OBJECT *obj)
|
|
||||||
+{
|
|
||||||
+ int ok;
|
|
||||||
+ X509_OBJECT *ret = X509_OBJECT_new();
|
|
||||||
+ if (ret == NULL) {
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
+ switch (X509_OBJECT_get_type(obj)) {
|
|
||||||
+ case X509_LU_X509:
|
|
||||||
+ ok = X509_OBJECT_set1_X509(ret, X509_OBJECT_get0_X509(obj));
|
|
||||||
+ break;
|
|
||||||
+ case X509_LU_CRL:
|
|
||||||
+ /* X509_OBJECT_get0_X509_CRL was not const-correct prior to 3.0.*/
|
|
||||||
+ ok = X509_OBJECT_set1_X509_CRL(
|
|
||||||
+ ret, X509_OBJECT_get0_X509_CRL((X509_OBJECT *)obj));
|
|
||||||
+ break;
|
|
||||||
+ default:
|
|
||||||
+ /* We cannot duplicate unrecognized types in a polyfill, but it is
|
|
||||||
+ * safe to leave an empty object. The caller will ignore it. */
|
|
||||||
+ ok = 1;
|
|
||||||
+ break;
|
|
||||||
+ }
|
|
||||||
+ if (!ok) {
|
|
||||||
+ X509_OBJECT_free(ret);
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
+ return ret;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+static STACK_OF(X509_OBJECT) *
|
|
||||||
+X509_STORE_get1_objects(X509_STORE *store)
|
|
||||||
+{
|
|
||||||
+ STACK_OF(X509_OBJECT) *ret;
|
|
||||||
+ if (!X509_STORE_lock(store)) {
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
+ ret = sk_X509_OBJECT_deep_copy(X509_STORE_get0_objects(store),
|
|
||||||
+ x509_object_dup, X509_OBJECT_free);
|
|
||||||
+ X509_STORE_unlock(store);
|
|
||||||
+ return ret;
|
|
||||||
+}
|
|
||||||
+#endif
|
|
||||||
+
|
|
||||||
PyDoc_STRVAR(PySSLContext_sni_callback_doc,
|
|
||||||
"Set a callback that will be called when a server name is provided by the SSL/TLS client in the SNI extension.\n\
|
|
||||||
\n\
|
|
||||||
@@ -4707,7 +4769,15 @@ _ssl__SSLContext_cert_store_stats_impl(P
|
|
||||||
int x509 = 0, crl = 0, ca = 0, i;
|
|
||||||
|
|
||||||
store = SSL_CTX_get_cert_store(self->ctx);
|
|
||||||
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
|
|
||||||
+ objs = X509_STORE_get1_objects(store);
|
|
||||||
+ if (objs == NULL) {
|
|
||||||
+ PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
+#else
|
|
||||||
objs = X509_STORE_get0_objects(store);
|
|
||||||
+#endif
|
|
||||||
for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
|
|
||||||
obj = sk_X509_OBJECT_value(objs, i);
|
|
||||||
switch (X509_OBJECT_get_type(obj)) {
|
|
||||||
@@ -4721,12 +4791,13 @@ _ssl__SSLContext_cert_store_stats_impl(P
|
|
||||||
crl++;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
- /* Ignore X509_LU_FAIL, X509_LU_RETRY, X509_LU_PKEY.
|
|
||||||
- * As far as I can tell they are internal states and never
|
|
||||||
- * stored in a cert store */
|
|
||||||
+ /* Ignore unrecognized types. */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
|
|
||||||
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
|
|
||||||
+#endif
|
|
||||||
return Py_BuildValue("{sisisi}", "x509", x509, "crl", crl,
|
|
||||||
"x509_ca", ca);
|
|
||||||
}
|
|
||||||
@@ -4758,7 +4829,15 @@ _ssl__SSLContext_get_ca_certs_impl(PySSL
|
|
||||||
}
|
|
||||||
|
|
||||||
store = SSL_CTX_get_cert_store(self->ctx);
|
|
||||||
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
|
|
||||||
+ objs = X509_STORE_get1_objects(store);
|
|
||||||
+ if (objs == NULL) {
|
|
||||||
+ PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
|
|
||||||
+ return NULL;
|
|
||||||
+ }
|
|
||||||
+#else
|
|
||||||
objs = X509_STORE_get0_objects(store);
|
|
||||||
+#endif
|
|
||||||
for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
|
|
||||||
X509_OBJECT *obj;
|
|
||||||
X509 *cert;
|
|
||||||
@@ -4786,9 +4865,15 @@ _ssl__SSLContext_get_ca_certs_impl(PySSL
|
|
||||||
}
|
|
||||||
Py_CLEAR(ci);
|
|
||||||
}
|
|
||||||
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
|
|
||||||
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
|
|
||||||
+#endif
|
|
||||||
return rlist;
|
|
||||||
|
|
||||||
error:
|
|
||||||
+#if HAVE_OPENSSL_X509_STORE_GET1_OBJECTS
|
|
||||||
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
|
|
||||||
+#endif
|
|
||||||
Py_XDECREF(ci);
|
|
||||||
Py_XDECREF(rlist);
|
|
||||||
return NULL;
|
|
@ -1,444 +0,0 @@
|
|||||||
From 05a14677846ed0a35773cd2ba582f9a65a3dfa48 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Petr Viktorin <encukou@gmail.com>
|
|
||||||
Date: Wed, 24 Apr 2024 14:29:30 +0200
|
|
||||||
Subject: [PATCH 1/3] gh-113171: gh-65056: Fix "private" (non-global) IP
|
|
||||||
address ranges (GH-113179) (GH-113186) (GH-118177)
|
|
||||||
|
|
||||||
* GH-113171: Fix "private" (non-global) IP address ranges (GH-113179)
|
|
||||||
|
|
||||||
The _private_networks variables, used by various is_private
|
|
||||||
implementations, were missing some ranges and at the same time had
|
|
||||||
overly strict ranges (where there are more specific ranges considered
|
|
||||||
globally reachable by the IANA registries).
|
|
||||||
|
|
||||||
This patch updates the ranges with what was missing or otherwise
|
|
||||||
incorrect.
|
|
||||||
|
|
||||||
100.64.0.0/10 is left alone, for now, as it's been made special in [1].
|
|
||||||
|
|
||||||
The _address_exclude_many() call returns 8 networks for IPv4, 121
|
|
||||||
networks for IPv6.
|
|
||||||
|
|
||||||
[1] https://github.com/python/cpython/issues/61602
|
|
||||||
|
|
||||||
* GH-65056: Improve the IP address' is_global/is_private documentation (GH-113186)
|
|
||||||
|
|
||||||
It wasn't clear what the semantics of is_global/is_private are and, when
|
|
||||||
one gets to the bottom of it, it's not quite so simple (hence the
|
|
||||||
exceptions listed).
|
|
||||||
|
|
||||||
(cherry picked from commit 2a4cbf17af19a01d942f9579342f77c39fbd23c4)
|
|
||||||
(cherry picked from commit 40d75c2b7f5c67e254d0a025e0f2e2c7ada7f69f)
|
|
||||||
|
|
||||||
---------
|
|
||||||
|
|
||||||
(cherry picked from commit f86b17ac511e68192ba71f27e752321a3252cee3)
|
|
||||||
|
|
||||||
Co-authored-by: Jakub Stasiak <jakub@stasiak.at>
|
|
||||||
---
|
|
||||||
Doc/library/ipaddress.rst | 43 ++++++++-
|
|
||||||
Doc/whatsnew/3.8.rst | 9 ++
|
|
||||||
Lib/ipaddress.py | 95 +++++++++++++++----
|
|
||||||
Lib/test/test_ipaddress.py | 52 ++++++++++
|
|
||||||
...-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 9 ++
|
|
||||||
5 files changed, 187 insertions(+), 21 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
|
|
||||||
|
|
||||||
diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst
|
|
||||||
index 5e21d5db2ed9c3..5944e33bb1b339 100644
|
|
||||||
--- a/Doc/library/ipaddress.rst
|
|
||||||
+++ b/Doc/library/ipaddress.rst
|
|
||||||
@@ -179,18 +179,53 @@ write code that handles both IP versions correctly. Address objects are
|
|
||||||
|
|
||||||
.. attribute:: is_private
|
|
||||||
|
|
||||||
- ``True`` if the address is allocated for private networks. See
|
|
||||||
+ ``True`` if the address is defined as not globally reachable by
|
|
||||||
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
|
|
||||||
- (for IPv6).
|
|
||||||
+ (for IPv6) with the following exceptions:
|
|
||||||
+
|
|
||||||
+ * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``)
|
|
||||||
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
|
|
||||||
+ semantics of the underlying IPv4 addresses and the following condition holds
|
|
||||||
+ (see :attr:`IPv6Address.ipv4_mapped`)::
|
|
||||||
+
|
|
||||||
+ address.is_private == address.ipv4_mapped.is_private
|
|
||||||
+
|
|
||||||
+ ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space
|
|
||||||
+ (``100.64.0.0/10`` range) where they are both ``False``.
|
|
||||||
+
|
|
||||||
+ .. versionchanged:: 3.8.20
|
|
||||||
+
|
|
||||||
+ Fixed some false positives and false negatives.
|
|
||||||
+
|
|
||||||
+ * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and
|
|
||||||
+ ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private).
|
|
||||||
+ * ``64:ff9b:1::/48`` is considered private.
|
|
||||||
+ * ``2002::/16`` is considered private.
|
|
||||||
+ * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``,
|
|
||||||
+ ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``.
|
|
||||||
+ The exceptions are not considered private.
|
|
||||||
|
|
||||||
.. attribute:: is_global
|
|
||||||
|
|
||||||
- ``True`` if the address is allocated for public networks. See
|
|
||||||
+ ``True`` if the address is defined as globally reachable by
|
|
||||||
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
|
|
||||||
- (for IPv6).
|
|
||||||
+ (for IPv6) with the following exception:
|
|
||||||
+
|
|
||||||
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
|
|
||||||
+ semantics of the underlying IPv4 addresses and the following condition holds
|
|
||||||
+ (see :attr:`IPv6Address.ipv4_mapped`)::
|
|
||||||
+
|
|
||||||
+ address.is_global == address.ipv4_mapped.is_global
|
|
||||||
+
|
|
||||||
+ ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space
|
|
||||||
+ (``100.64.0.0/10`` range) where they are both ``False``.
|
|
||||||
|
|
||||||
.. versionadded:: 3.4
|
|
||||||
|
|
||||||
+ .. versionchanged:: 3.8.20
|
|
||||||
+
|
|
||||||
+ Fixed some false positives and false negatives, see :attr:`is_private` for details.
|
|
||||||
+
|
|
||||||
.. attribute:: is_unspecified
|
|
||||||
|
|
||||||
``True`` if the address is unspecified. See :RFC:`5735` (for IPv4)
|
|
||||||
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
|
|
||||||
index e5278da3f6a5be..de4dd856877543 100644
|
|
||||||
--- a/Doc/whatsnew/3.8.rst
|
|
||||||
+++ b/Doc/whatsnew/3.8.rst
|
|
||||||
@@ -2355,3 +2355,12 @@ tarfile
|
|
||||||
:exc:`DeprecationWarning`.
|
|
||||||
In Python 3.14, the default will switch to ``'data'``.
|
|
||||||
(Contributed by Petr Viktorin in :pep:`706`.)
|
|
||||||
+
|
|
||||||
+Notable changes in 3.8.20
|
|
||||||
+=========================
|
|
||||||
+
|
|
||||||
+ipaddress
|
|
||||||
+---------
|
|
||||||
+
|
|
||||||
+* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
|
|
||||||
+ ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
|
|
||||||
diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py
|
|
||||||
index d351f07a5bd960..142c3b13b1617e 100644
|
|
||||||
--- a/Lib/ipaddress.py
|
|
||||||
+++ b/Lib/ipaddress.py
|
|
||||||
@@ -1275,18 +1275,41 @@ def is_reserved(self):
|
|
||||||
@property
|
|
||||||
@functools.lru_cache()
|
|
||||||
def is_private(self):
|
|
||||||
- """Test if this address is allocated for private networks.
|
|
||||||
+ """``True`` if the address is defined as not globally reachable by
|
|
||||||
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
|
|
||||||
+ (for IPv6) with the following exceptions:
|
|
||||||
|
|
||||||
- Returns:
|
|
||||||
- A boolean, True if the address is reserved per
|
|
||||||
- iana-ipv4-special-registry.
|
|
||||||
+ * ``is_private`` is ``False`` for ``100.64.0.0/10``
|
|
||||||
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
|
|
||||||
+ semantics of the underlying IPv4 addresses and the following condition holds
|
|
||||||
+ (see :attr:`IPv6Address.ipv4_mapped`)::
|
|
||||||
+
|
|
||||||
+ address.is_private == address.ipv4_mapped.is_private
|
|
||||||
|
|
||||||
+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
|
|
||||||
+ IPv4 range where they are both ``False``.
|
|
||||||
"""
|
|
||||||
- return any(self in net for net in self._constants._private_networks)
|
|
||||||
+ return (
|
|
||||||
+ any(self in net for net in self._constants._private_networks)
|
|
||||||
+ and all(self not in net for net in self._constants._private_networks_exceptions)
|
|
||||||
+ )
|
|
||||||
|
|
||||||
@property
|
|
||||||
@functools.lru_cache()
|
|
||||||
def is_global(self):
|
|
||||||
+ """``True`` if the address is defined as globally reachable by
|
|
||||||
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
|
|
||||||
+ (for IPv6) with the following exception:
|
|
||||||
+
|
|
||||||
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
|
|
||||||
+ semantics of the underlying IPv4 addresses and the following condition holds
|
|
||||||
+ (see :attr:`IPv6Address.ipv4_mapped`)::
|
|
||||||
+
|
|
||||||
+ address.is_global == address.ipv4_mapped.is_global
|
|
||||||
+
|
|
||||||
+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
|
|
||||||
+ IPv4 range where they are both ``False``.
|
|
||||||
+ """
|
|
||||||
return self not in self._constants._public_network and not self.is_private
|
|
||||||
|
|
||||||
@property
|
|
||||||
@@ -1490,13 +1513,15 @@ class _IPv4Constants:
|
|
||||||
|
|
||||||
_public_network = IPv4Network('100.64.0.0/10')
|
|
||||||
|
|
||||||
+ # Not globally reachable address blocks listed on
|
|
||||||
+ # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
|
|
||||||
_private_networks = [
|
|
||||||
IPv4Network('0.0.0.0/8'),
|
|
||||||
IPv4Network('10.0.0.0/8'),
|
|
||||||
IPv4Network('127.0.0.0/8'),
|
|
||||||
IPv4Network('169.254.0.0/16'),
|
|
||||||
IPv4Network('172.16.0.0/12'),
|
|
||||||
- IPv4Network('192.0.0.0/29'),
|
|
||||||
+ IPv4Network('192.0.0.0/24'),
|
|
||||||
IPv4Network('192.0.0.170/31'),
|
|
||||||
IPv4Network('192.0.2.0/24'),
|
|
||||||
IPv4Network('192.168.0.0/16'),
|
|
||||||
@@ -1507,6 +1532,11 @@ class _IPv4Constants:
|
|
||||||
IPv4Network('255.255.255.255/32'),
|
|
||||||
]
|
|
||||||
|
|
||||||
+ _private_networks_exceptions = [
|
|
||||||
+ IPv4Network('192.0.0.9/32'),
|
|
||||||
+ IPv4Network('192.0.0.10/32'),
|
|
||||||
+ ]
|
|
||||||
+
|
|
||||||
_reserved_network = IPv4Network('240.0.0.0/4')
|
|
||||||
|
|
||||||
_unspecified_address = IPv4Address('0.0.0.0')
|
|
||||||
@@ -1897,23 +1927,42 @@ def is_site_local(self):
|
|
||||||
@property
|
|
||||||
@functools.lru_cache()
|
|
||||||
def is_private(self):
|
|
||||||
- """Test if this address is allocated for private networks.
|
|
||||||
+ """``True`` if the address is defined as not globally reachable by
|
|
||||||
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
|
|
||||||
+ (for IPv6) with the following exceptions:
|
|
||||||
|
|
||||||
- Returns:
|
|
||||||
- A boolean, True if the address is reserved per
|
|
||||||
- iana-ipv6-special-registry.
|
|
||||||
+ * ``is_private`` is ``False`` for ``100.64.0.0/10``
|
|
||||||
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
|
|
||||||
+ semantics of the underlying IPv4 addresses and the following condition holds
|
|
||||||
+ (see :attr:`IPv6Address.ipv4_mapped`)::
|
|
||||||
+
|
|
||||||
+ address.is_private == address.ipv4_mapped.is_private
|
|
||||||
|
|
||||||
+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
|
|
||||||
+ IPv4 range where they are both ``False``.
|
|
||||||
"""
|
|
||||||
- return any(self in net for net in self._constants._private_networks)
|
|
||||||
+ ipv4_mapped = self.ipv4_mapped
|
|
||||||
+ if ipv4_mapped is not None:
|
|
||||||
+ return ipv4_mapped.is_private
|
|
||||||
+ return (
|
|
||||||
+ any(self in net for net in self._constants._private_networks)
|
|
||||||
+ and all(self not in net for net in self._constants._private_networks_exceptions)
|
|
||||||
+ )
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_global(self):
|
|
||||||
- """Test if this address is allocated for public networks.
|
|
||||||
+ """``True`` if the address is defined as globally reachable by
|
|
||||||
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
|
|
||||||
+ (for IPv6) with the following exception:
|
|
||||||
|
|
||||||
- Returns:
|
|
||||||
- A boolean, true if the address is not reserved per
|
|
||||||
- iana-ipv6-special-registry.
|
|
||||||
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
|
|
||||||
+ semantics of the underlying IPv4 addresses and the following condition holds
|
|
||||||
+ (see :attr:`IPv6Address.ipv4_mapped`)::
|
|
||||||
+
|
|
||||||
+ address.is_global == address.ipv4_mapped.is_global
|
|
||||||
|
|
||||||
+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
|
|
||||||
+ IPv4 range where they are both ``False``.
|
|
||||||
"""
|
|
||||||
return not self.is_private
|
|
||||||
|
|
||||||
@@ -2154,19 +2203,31 @@ class _IPv6Constants:
|
|
||||||
|
|
||||||
_multicast_network = IPv6Network('ff00::/8')
|
|
||||||
|
|
||||||
+ # Not globally reachable address blocks listed on
|
|
||||||
+ # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
|
|
||||||
_private_networks = [
|
|
||||||
IPv6Network('::1/128'),
|
|
||||||
IPv6Network('::/128'),
|
|
||||||
IPv6Network('::ffff:0:0/96'),
|
|
||||||
+ IPv6Network('64:ff9b:1::/48'),
|
|
||||||
IPv6Network('100::/64'),
|
|
||||||
IPv6Network('2001::/23'),
|
|
||||||
- IPv6Network('2001:2::/48'),
|
|
||||||
IPv6Network('2001:db8::/32'),
|
|
||||||
- IPv6Network('2001:10::/28'),
|
|
||||||
+ # IANA says N/A, let's consider it not globally reachable to be safe
|
|
||||||
+ IPv6Network('2002::/16'),
|
|
||||||
IPv6Network('fc00::/7'),
|
|
||||||
IPv6Network('fe80::/10'),
|
|
||||||
]
|
|
||||||
|
|
||||||
+ _private_networks_exceptions = [
|
|
||||||
+ IPv6Network('2001:1::1/128'),
|
|
||||||
+ IPv6Network('2001:1::2/128'),
|
|
||||||
+ IPv6Network('2001:3::/32'),
|
|
||||||
+ IPv6Network('2001:4:112::/48'),
|
|
||||||
+ IPv6Network('2001:20::/28'),
|
|
||||||
+ IPv6Network('2001:30::/28'),
|
|
||||||
+ ]
|
|
||||||
+
|
|
||||||
_reserved_networks = [
|
|
||||||
IPv6Network('::/8'), IPv6Network('100::/8'),
|
|
||||||
IPv6Network('200::/7'), IPv6Network('400::/6'),
|
|
||||||
diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py
|
|
||||||
index 1297b8371d8583..46002111b3270a 100644
|
|
||||||
--- a/Lib/test/test_ipaddress.py
|
|
||||||
+++ b/Lib/test/test_ipaddress.py
|
|
||||||
@@ -1761,6 +1761,10 @@ def testReservedIpv4(self):
|
|
||||||
self.assertEqual(True, ipaddress.ip_address(
|
|
||||||
'172.31.255.255').is_private)
|
|
||||||
self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
|
|
||||||
|
|
||||||
self.assertEqual(True,
|
|
||||||
ipaddress.ip_address('169.254.100.200').is_link_local)
|
|
||||||
@@ -1776,6 +1780,40 @@ def testReservedIpv4(self):
|
|
||||||
self.assertEqual(False, ipaddress.ip_address('128.0.0.0').is_loopback)
|
|
||||||
self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified)
|
|
||||||
|
|
||||||
+ def testPrivateNetworks(self):
|
|
||||||
+ self.assertEqual(False, ipaddress.ip_network("0.0.0.0/0").is_private)
|
|
||||||
+ self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private)
|
|
||||||
+
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
|
|
||||||
+ self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("203.0.113.0/24").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("240.0.0.0/4").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("255.255.255.255/32").is_private)
|
|
||||||
+
|
|
||||||
+ self.assertEqual(False, ipaddress.ip_network("::/0").is_private)
|
|
||||||
+ self.assertEqual(False, ipaddress.ip_network("::ff/128").is_private)
|
|
||||||
+
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("::1/128").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
|
|
||||||
+ self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private)
|
|
||||||
+
|
|
||||||
def testReservedIpv6(self):
|
|
||||||
|
|
||||||
self.assertEqual(True, ipaddress.ip_network('ffff::').is_multicast)
|
|
||||||
@@ -1849,6 +1887,20 @@ def testReservedIpv6(self):
|
|
||||||
self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
|
|
||||||
self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
|
|
||||||
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('2001::').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
|
|
||||||
+ self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
|
|
||||||
+ self.assertFalse(ipaddress.ip_address('2002::').is_global)
|
|
||||||
+
|
|
||||||
# some generic IETF reserved addresses
|
|
||||||
self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
|
|
||||||
self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)
|
|
||||||
diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
|
|
||||||
new file mode 100644
|
|
||||||
index 00000000000000..f9a72473be4e2c
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
|
|
||||||
@@ -0,0 +1,9 @@
|
|
||||||
+Fixed various false positives and false negatives in
|
|
||||||
+
|
|
||||||
+* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
|
|
||||||
+* :attr:`ipaddress.IPv4Address.is_global`
|
|
||||||
+* :attr:`ipaddress.IPv6Address.is_private`
|
|
||||||
+* :attr:`ipaddress.IPv6Address.is_global`
|
|
||||||
+
|
|
||||||
+Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
|
|
||||||
+attributes.
|
|
||||||
|
|
||||||
From 2e92223a4298fbf18c1c7f853b6d883943c00c52 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Petr Viktorin <encukou@gmail.com>
|
|
||||||
Date: Wed, 24 Apr 2024 15:16:13 +0200
|
|
||||||
Subject: [PATCH 2/3] Adjust test for 3.10 semantics of is_private on networks
|
|
||||||
|
|
||||||
In 3.10 and below, is_private checks whether the network and broadcast
|
|
||||||
address are both private.
|
|
||||||
In later versions (where the test wss backported from), it checks
|
|
||||||
whether they both are in the same private network.
|
|
||||||
|
|
||||||
For 0.0.0.0/0, both 0.0.0.0 and 255.225.255.255 are private,
|
|
||||||
but one is in 0.0.0.0/8 ("This network") and the other in
|
|
||||||
255.255.255.255/32 ("Limited broadcast").
|
|
||||||
---
|
|
||||||
Lib/test/test_ipaddress.py | 2 +-
|
|
||||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py
|
|
||||||
index 46002111b3270a..fad40334f0d317 100644
|
|
||||||
--- a/Lib/test/test_ipaddress.py
|
|
||||||
+++ b/Lib/test/test_ipaddress.py
|
|
||||||
@@ -1781,7 +1781,7 @@ def testReservedIpv4(self):
|
|
||||||
self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified)
|
|
||||||
|
|
||||||
def testPrivateNetworks(self):
|
|
||||||
- self.assertEqual(False, ipaddress.ip_network("0.0.0.0/0").is_private)
|
|
||||||
+ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/0").is_private)
|
|
||||||
self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private)
|
|
||||||
|
|
||||||
self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private)
|
|
||||||
|
|
||||||
From e366724f6e290b71ec49005079e8472c3cac2594 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Petr Viktorin <encukou@gmail.com>
|
|
||||||
Date: Wed, 1 May 2024 15:29:13 +0200
|
|
||||||
Subject: [PATCH 3/3] Add IPv6 addresses to suspignore.csv
|
|
||||||
|
|
||||||
That's a lot of semicolons!
|
|
||||||
---
|
|
||||||
Doc/tools/susp-ignored.csv | 8 ++++++++
|
|
||||||
1 file changed, 8 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/Doc/tools/susp-ignored.csv b/Doc/tools/susp-ignored.csv
|
|
||||||
index dd6aa38d72adcc..fadeab3f98f2d7 100644
|
|
||||||
--- a/Doc/tools/susp-ignored.csv
|
|
||||||
+++ b/Doc/tools/susp-ignored.csv
|
|
||||||
@@ -158,6 +158,14 @@ library/ipaddress,,:db00,2001:db00::0/24
|
|
||||||
library/ipaddress,,::,2001:db00::0/24
|
|
||||||
library/ipaddress,,:db00,2001:db00::0/ffff:ff00::
|
|
||||||
library/ipaddress,,::,2001:db00::0/ffff:ff00::
|
|
||||||
+library/ipaddress,,:ff9b,64:ff9b:1::/48
|
|
||||||
+library/ipaddress,,::,64:ff9b:1::/48
|
|
||||||
+library/ipaddress,,::,2001::
|
|
||||||
+library/ipaddress,,::,2001:1::
|
|
||||||
+library/ipaddress,,::,2001:3::
|
|
||||||
+library/ipaddress,,::,2001:4:112::
|
|
||||||
+library/ipaddress,,::,2001:20::
|
|
||||||
+library/ipaddress,,::,2001:30::
|
|
||||||
library/itertools,,:step,elements from seq[start:stop:step]
|
|
||||||
library/itertools,,:stop,elements from seq[start:stop:step]
|
|
||||||
library/logging.handlers,,:port,host:port
|
|
@ -1,338 +0,0 @@
|
|||||||
From d7cf62cf9f630975a0e876708c4a23907a23aba3 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Petr Viktorin <encukou@gmail.com>
|
|
||||||
Date: Wed, 31 Jul 2024 00:19:48 +0200
|
|
||||||
Subject: [PATCH 1/4] [3.8] gh-121650: Encode newlines in headers, and verify
|
|
||||||
headers are sound (GH-122233)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
This should fail for custom fold() implementations that aren't careful
|
|
||||||
about newlines.
|
|
||||||
|
|
||||||
(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
|
|
||||||
|
|
||||||
Co-authored-by: Petr Viktorin <encukou@gmail.com>
|
|
||||||
Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
|
|
||||||
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
|
|
||||||
---
|
|
||||||
Doc/library/email.errors.rst | 6
|
|
||||||
Doc/library/email.policy.rst | 18 ++
|
|
||||||
Doc/whatsnew/3.8.rst | 12 +
|
|
||||||
Lib/email/_header_value_parser.py | 12 +
|
|
||||||
Lib/email/_policybase.py | 8 +
|
|
||||||
Lib/email/errors.py | 4
|
|
||||||
Lib/email/generator.py | 16 ++
|
|
||||||
Lib/test/test_email/test_generator.py | 62 ++++++++++
|
|
||||||
Lib/test/test_email/test_policy.py | 26 ++++
|
|
||||||
Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst | 5
|
|
||||||
10 files changed, 165 insertions(+), 4 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
|
|
||||||
|
|
||||||
--- a/Doc/library/email.errors.rst
|
|
||||||
+++ b/Doc/library/email.errors.rst
|
|
||||||
@@ -59,6 +59,12 @@ The following exception classes are defi
|
|
||||||
: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
|
|
||||||
where the problem was found, so for example, if a message nested inside a
|
|
||||||
--- 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.8.20
|
|
||||||
+
|
|
||||||
+
|
|
||||||
The following :class:`Policy` method is intended to be called by code using
|
|
||||||
the email library to create policy instances with custom settings:
|
|
||||||
|
|
||||||
--- a/Doc/whatsnew/3.8.rst
|
|
||||||
+++ b/Doc/whatsnew/3.8.rst
|
|
||||||
@@ -2364,3 +2364,15 @@ ipaddress
|
|
||||||
|
|
||||||
* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
|
|
||||||
``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
|
|
||||||
+
|
|
||||||
+email
|
|
||||||
+-----
|
|
||||||
+
|
|
||||||
+* Headers with embedded newlines are now quoted on output.
|
|
||||||
+
|
|
||||||
+ The :mod:`~email.generator` will now refuse to serialize (write) headers
|
|
||||||
+ that are improperly folded or delimited, such that they would be parsed as
|
|
||||||
+ multiple headers or joined with adjacent data.
|
|
||||||
+ If you need to turn this safety feature off,
|
|
||||||
+ set :attr:`~email.policy.Policy.verify_generated_headers`.
|
|
||||||
+ (Contributed by Bas Bloemsaat and Petr Viktorin in :gh:`121650`.)
|
|
||||||
--- a/Lib/email/_header_value_parser.py
|
|
||||||
+++ b/Lib/email/_header_value_parser.py
|
|
||||||
@@ -92,6 +92,8 @@ TOKEN_ENDS = TSPECIALS | WSP
|
|
||||||
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, *, po
|
|
||||||
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
|
|
||||||
--- a/Lib/email/_policybase.py
|
|
||||||
+++ b/Lib/email/_policybase.py
|
|
||||||
@@ -157,6 +157,13 @@ class Policy(_PolicyBase, metaclass=abc.
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
--- 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."""
|
|
||||||
--- a/Lib/email/generator.py
|
|
||||||
+++ b/Lib/email/generator.py
|
|
||||||
@@ -14,12 +14,14 @@ import random
|
|
||||||
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,19 @@ class Generator:
|
|
||||||
|
|
||||||
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}')
|
|
||||||
+ folded_no_linesep = folded
|
|
||||||
+ if folded.endswith(linesep):
|
|
||||||
+ folded_no_linesep = folded[:-len(linesep)]
|
|
||||||
+ if NEWLINE_WITHOUT_FWSP.search(folded_no_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)
|
|
||||||
|
|
||||||
--- a/Lib/test/test_email/test_generator.py
|
|
||||||
+++ b/Lib/test/test_email/test_generator.py
|
|
||||||
@@ -6,6 +6,7 @@ from email.message import EmailMessage
|
|
||||||
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 @@ class TestGeneratorBase:
|
|
||||||
g.flatten(msg)
|
|
||||||
self.assertEqual(s.getvalue(), self.typ(expected))
|
|
||||||
|
|
||||||
+ def test_keep_encoded_newlines(self):
|
|
||||||
+ msg = self.msgmaker(self.typ(textwrap.dedent("""\
|
|
||||||
+ To: nobody
|
|
||||||
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
|
|
||||||
+
|
|
||||||
+ None
|
|
||||||
+ """)))
|
|
||||||
+ expected = textwrap.dedent("""\
|
|
||||||
+ To: nobody
|
|
||||||
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
|
|
||||||
+
|
|
||||||
+ None
|
|
||||||
+ """)
|
|
||||||
+ s = self.ioclass()
|
|
||||||
+ g = self.genclass(s, policy=self.policy.clone(max_line_length=80))
|
|
||||||
+ g.flatten(msg)
|
|
||||||
+ self.assertEqual(s.getvalue(), self.typ(expected))
|
|
||||||
+
|
|
||||||
+ def test_keep_long_encoded_newlines(self):
|
|
||||||
+ msg = self.msgmaker(self.typ(textwrap.dedent("""\
|
|
||||||
+ To: nobody
|
|
||||||
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
|
|
||||||
+
|
|
||||||
+ None
|
|
||||||
+ """)))
|
|
||||||
+ expected = textwrap.dedent("""\
|
|
||||||
+ To: nobody
|
|
||||||
+ Subject: Bad subject
|
|
||||||
+ =?utf-8?q?=0A?=Bcc:
|
|
||||||
+ injection@example.com
|
|
||||||
+
|
|
||||||
+ None
|
|
||||||
+ """)
|
|
||||||
+ s = self.ioclass()
|
|
||||||
+ g = self.genclass(s, policy=self.policy.clone(max_line_length=30))
|
|
||||||
+ g.flatten(msg)
|
|
||||||
+ self.assertEqual(s.getvalue(), self.typ(expected))
|
|
||||||
+
|
|
||||||
|
|
||||||
class TestGenerator(TestGeneratorBase, TestEmailBase):
|
|
||||||
|
|
||||||
@@ -224,6 +263,29 @@ class TestGenerator(TestGeneratorBase, T
|
|
||||||
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):
|
|
||||||
|
|
||||||
--- 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 @@ class PolicyAPITests(unittest.TestCase):
|
|
||||||
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).
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
|
|
||||||
@@ -0,0 +1,5 @@
|
|
||||||
+:mod:`email` headers with embedded newlines are now quoted on output. The
|
|
||||||
+:mod:`~email.generator` will now refuse to serialize (write) headers that
|
|
||||||
+are unsafely folded or delimited; see
|
|
||||||
+:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas
|
|
||||||
+Bloemsaat and Petr Viktorin in :gh:`121650`.)
|
|
@ -1,142 +0,0 @@
|
|||||||
From dcb320a0c85713c5dfe89a83d6eb295ad1511be8 Mon Sep 17 00:00:00 2001
|
|
||||||
From: "Jason R. Coombs" <jaraco@jaraco.com>
|
|
||||||
Date: Tue, 27 Aug 2024 17:10:30 -0400
|
|
||||||
Subject: [PATCH] [3.8] [3.9] [3.11] gh-123270: Replaced SanitizedNames with a
|
|
||||||
more surgical fix. (GH-123354)
|
|
||||||
|
|
||||||
Applies changes from zipp 3.20.1 and jaraco/zippGH-124
|
|
||||||
(cherry picked from commit 2231286d78d328c2f575e0b05b16fe447d1656d6)
|
|
||||||
(cherry picked from commit 17b77bb41409259bad1cd6c74761c18b6ab1e860)
|
|
||||||
(cherry picked from commit 66d3383)
|
|
||||||
|
|
||||||
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
|
|
||||||
---
|
|
||||||
Lib/test/test_zipfile.py | 75 ++++++++++
|
|
||||||
Lib/zipfile.py | 9 -
|
|
||||||
Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst | 3
|
|
||||||
3 files changed, 85 insertions(+), 2 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst
|
|
||||||
|
|
||||||
Index: Python-3.8.19/Lib/test/test_zipfile.py
|
|
||||||
===================================================================
|
|
||||||
--- Python-3.8.19.orig/Lib/test/test_zipfile.py
|
|
||||||
+++ Python-3.8.19/Lib/test/test_zipfile.py
|
|
||||||
@@ -3007,6 +3007,81 @@ class TestPath(unittest.TestCase):
|
|
||||||
data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
|
|
||||||
zipfile.CompleteDirs._implied_dirs(data)
|
|
||||||
|
|
||||||
+ def test_malformed_paths(self):
|
|
||||||
+ """
|
|
||||||
+ Path should handle malformed paths gracefully.
|
|
||||||
+
|
|
||||||
+ Paths with leading slashes are not visible.
|
|
||||||
+
|
|
||||||
+ Paths with dots are treated like regular files.
|
|
||||||
+ """
|
|
||||||
+ data = io.BytesIO()
|
|
||||||
+ zf = zipfile.ZipFile(data, "w")
|
|
||||||
+ zf.writestr("../parent.txt", b"content")
|
|
||||||
+ zf.filename = ''
|
|
||||||
+ root = zipfile.Path(zf)
|
|
||||||
+ assert list(map(str, root.iterdir())) == ['../']
|
|
||||||
+ assert root.joinpath('..').joinpath('parent.txt').read_bytes() == b'content'
|
|
||||||
+
|
|
||||||
+ def test_unsupported_names(self):
|
|
||||||
+ """
|
|
||||||
+ Path segments with special characters are readable.
|
|
||||||
+
|
|
||||||
+ On some platforms or file systems, characters like
|
|
||||||
+ ``:`` and ``?`` are not allowed, but they are valid
|
|
||||||
+ in the zip file.
|
|
||||||
+ """
|
|
||||||
+ data = io.BytesIO()
|
|
||||||
+ zf = zipfile.ZipFile(data, "w")
|
|
||||||
+ zf.writestr("path?", b"content")
|
|
||||||
+ zf.writestr("V: NMS.flac", b"fLaC...")
|
|
||||||
+ zf.filename = ''
|
|
||||||
+ root = zipfile.Path(zf)
|
|
||||||
+ contents = root.iterdir()
|
|
||||||
+ assert next(contents).name == 'path?'
|
|
||||||
+ assert next(contents).name == 'V: NMS.flac'
|
|
||||||
+ assert root.joinpath('V: NMS.flac').read_bytes() == b"fLaC..."
|
|
||||||
+
|
|
||||||
+ def test_backslash_not_separator(self):
|
|
||||||
+ """
|
|
||||||
+ In a zip file, backslashes are not separators.
|
|
||||||
+ """
|
|
||||||
+ data = io.BytesIO()
|
|
||||||
+ zf = zipfile.ZipFile(data, "w")
|
|
||||||
+ zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
|
|
||||||
+ zf.filename = ''
|
|
||||||
+ root = zipfile.Path(zf)
|
|
||||||
+ (first,) = root.iterdir()
|
|
||||||
+ assert not first.is_dir()
|
|
||||||
+ assert first.name == 'foo\\bar'
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+class DirtyZipInfo(zipfile.ZipInfo):
|
|
||||||
+ """
|
|
||||||
+ Bypass name sanitization.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ def __init__(self, filename, *args, **kwargs):
|
|
||||||
+ super().__init__(filename, *args, **kwargs)
|
|
||||||
+ self.filename = filename
|
|
||||||
+
|
|
||||||
+ @classmethod
|
|
||||||
+ def for_name(cls, name, archive):
|
|
||||||
+ """
|
|
||||||
+ Construct the same way that ZipFile.writestr does.
|
|
||||||
+
|
|
||||||
+ TODO: extract this functionality and re-use
|
|
||||||
+ """
|
|
||||||
+ self = cls(filename=name, date_time=time.localtime(time.time())[:6])
|
|
||||||
+ self.compress_type = archive.compression
|
|
||||||
+ self.compress_level = archive.compresslevel
|
|
||||||
+ if self.filename.endswith('/'): # pragma: no cover
|
|
||||||
+ self.external_attr = 0o40775 << 16 # drwxrwxr-x
|
|
||||||
+ self.external_attr |= 0x10 # MS-DOS directory flag
|
|
||||||
+ else:
|
|
||||||
+ self.external_attr = 0o600 << 16 # ?rw-------
|
|
||||||
+ return self
|
|
||||||
+
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Index: Python-3.8.19/Lib/zipfile.py
|
|
||||||
===================================================================
|
|
||||||
--- Python-3.8.19.orig/Lib/zipfile.py
|
|
||||||
+++ Python-3.8.19/Lib/zipfile.py
|
|
||||||
@@ -2161,7 +2161,7 @@ def _parents(path):
|
|
||||||
def _ancestry(path):
|
|
||||||
"""
|
|
||||||
Given a path with elements separated by
|
|
||||||
- posixpath.sep, generate all elements of that path
|
|
||||||
+ posixpath.sep, generate all elements of that path.
|
|
||||||
|
|
||||||
>>> list(_ancestry('b/d'))
|
|
||||||
['b/d', 'b']
|
|
||||||
@@ -2173,9 +2173,14 @@ def _ancestry(path):
|
|
||||||
['b']
|
|
||||||
>>> list(_ancestry(''))
|
|
||||||
[]
|
|
||||||
+
|
|
||||||
+ Multiple separators are treated like a single.
|
|
||||||
+
|
|
||||||
+ >>> list(_ancestry('//b//d///f//'))
|
|
||||||
+ ['//b//d///f', '//b//d', '//b']
|
|
||||||
"""
|
|
||||||
path = path.rstrip(posixpath.sep)
|
|
||||||
- while path and path != posixpath.sep:
|
|
||||||
+ while path.rstrip(posixpath.sep):
|
|
||||||
yield path
|
|
||||||
path, tail = posixpath.split(path)
|
|
||||||
|
|
||||||
Index: Python-3.8.19/Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst
|
|
||||||
===================================================================
|
|
||||||
--- /dev/null
|
|
||||||
+++ Python-3.8.19/Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst
|
|
||||||
@@ -0,0 +1,3 @@
|
|
||||||
+Applied a more surgical fix for malformed payloads in :class:`zipfile.Path`
|
|
||||||
+causing infinite loops (gh-122905) without breaking contents using
|
|
||||||
+legitimate characters.
|
|
@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d2807ac69f69b84fd46a0b93bbd02a4fa48d3e70f4b2835ff0f72a2885040076
|
|
||||||
size 18975156
|
|
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQIzBAABCgAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmX5t/gACgkQsmmV4xAl
|
|
||||||
BWgW0RAAkQYR6L3LNvuAg3OS/wD6Kouv3CnXeAwYY/BHglsHawtz+gM4jZRK8fIo
|
|
||||||
vEKBk6uoZBvXX1yJR+cxLZOxb9K/X7zYJXyBxRav8veBzXePTVhJBNSS/ckE0ARN
|
|
||||||
bD8M2P/7byMlm616aNNE1hrIIaxNoX8/yTEK3DmISQonc8vCW6ygIXm3Vw/6rqG8
|
|
||||||
n16MGG2r4dNEI+pEs8LPj8/VBaHHkbyvK9y2DQ8ywBqsaE459bN4HdzTkMxh28s0
|
|
||||||
scDl33PwTabFgVUTXILs+vBNnHc6ylo6gEd6fAe7Epec5wnvexKykel9ZtidxHwB
|
|
||||||
KQl2YKErJGF97T1Aj/Cru82jBYS/YS2QVy2cX0sYhiTgOXsvB7vOViFESR3IlSEL
|
|
||||||
aQv+f+lBXZp8T4MbDuzz2H7dqNY0sYqmTcqJU9r4H+RGLw43PHLSRVfIDPiaheA+
|
|
||||||
n1ZYzzgfm2uucO+iIpDwAOvTWznj4YcFwX116fn2kJYLtJeI58wVIbtMTDCl/l9U
|
|
||||||
hNY+b5L5JsHlyoRSjDwAtQVBm3fS0YqV4OhWglhvvuEdobRK+F3+hmHvo18YxZyl
|
|
||||||
WXLBUwZy9LQoEyuc1YFemWYw7g3u1ru8WTCFtPm91OeErkKq3QuqwiCjROgUmN9D
|
|
||||||
xUypHTocPhkdF1yEVqG+HMDin9Rw+l2KMgFt5XLNYFvAycGlsk4=
|
|
||||||
=Uo2Y
|
|
||||||
-----END PGP SIGNATURE-----
|
|
3
Python-3.8.20.tar.xz
Normal file
3
Python-3.8.20.tar.xz
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6fb89a7124201c61125c0ab4cf7f6894df339a40c02833bfd28ab4d7691fafb4
|
||||||
|
size 18962788
|
16
Python-3.8.20.tar.xz.asc
Normal file
16
Python-3.8.20.tar.xz.asc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCAAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmbcKdUACgkQsmmV4xAl
|
||||||
|
BWhI0Q/+MWwQirWSn5ZApMU4EmKrZ0x8fuAeMHam6Dg1FaATYxJxgkf8ep5TN7Xp
|
||||||
|
VQpNVa40PqXbAf93x4KSavv/QS464Whv1PX5FsBkJBwzPPR3xY8CAfisPXchrOQV
|
||||||
|
12OKmhXnT7nebqaax56SLJsnen8EZB35X7d8RzDVqdNu+Xl7ICOY7d3LkPW14SGk
|
||||||
|
/pHgF2YgfClQSAh4MLRR0EYhUVlzKENJkGnMTdU9VicOnjJ5LtkGbHhVYPAEqALf
|
||||||
|
r6hrn2gyorPF8gtY+uN8d2/UKh9Xih/tRPhM3GGbX6Q9EBNpivvNK7829jEAkw1P
|
||||||
|
NZKtAOfiQGJU4I1PQkiYDMcbtc/fhDaj96nA+X3LxKih7dzLCXVGwDLsBYN6PJp4
|
||||||
|
ogR+nL7d5ZcikGrzHHaxZjncj2nPrbk5+aNoIZWHq29RiuZ0c74GRl9ILYYijk/E
|
||||||
|
vXsiBtuzwbA2UCF+gRVrZA3a9G4dMkLvftmbDemnUw5xGTWNEEInKD5Sv3fDhvMx
|
||||||
|
vdRI72RglYy1VNrgcG5mcH7bviXFy+voExUUjvsHOGkqHvyK+tVV0T+EMfEtRp6k
|
||||||
|
rCVdswN0LFIkmerVfr6XXHFty1mlKRvX+hyeiRXIhdFZieDQHdVH+3rvaft31LcQ
|
||||||
|
S2Mv+EGDeUP9llrF80Slbw7XPFfclkOBtzOOKwcTN+2NlpvczOw=
|
||||||
|
=9/j+
|
||||||
|
-----END PGP SIGNATURE-----
|
31
gh120226-fix-sendfile-test-kernel-610.patch
Normal file
31
gh120226-fix-sendfile-test-kernel-610.patch
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
From 1b3f6523a5c83323cdc44031b33a1c062e5dc698 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Xi Ruoyao <xry111@xry111.site>
|
||||||
|
Date: Fri, 7 Jun 2024 23:51:32 +0800
|
||||||
|
Subject: [PATCH] gh-120226: Fix
|
||||||
|
test_sendfile_close_peer_in_the_middle_of_receiving on Linux >= 6.10
|
||||||
|
(GH-120227)
|
||||||
|
|
||||||
|
The worst case is that the kernel buffers 17 pages with a page size of 64k.
|
||||||
|
(cherry picked from commit a7584245661102a5768c643fbd7db8395fd3c90e)
|
||||||
|
|
||||||
|
Co-authored-by: Xi Ruoyao <xry111@xry111.site>
|
||||||
|
---
|
||||||
|
Lib/test/test_asyncio/test_sendfile.py | 7 ++++---
|
||||||
|
1 file changed, 4 insertions(+), 3 deletions(-)
|
||||||
|
|
||||||
|
--- a/Lib/test/test_asyncio/test_sendfile.py
|
||||||
|
+++ b/Lib/test/test_asyncio/test_sendfile.py
|
||||||
|
@@ -86,9 +86,10 @@ class MyProto(asyncio.Protocol):
|
||||||
|
|
||||||
|
class SendfileBase:
|
||||||
|
|
||||||
|
- # 128 KiB plus small unaligned to buffer chunk
|
||||||
|
- DATA = b"SendfileBaseData" * (1024 * 8 + 1)
|
||||||
|
-
|
||||||
|
+ # Linux >= 6.10 seems buffering up to 17 pages of data.
|
||||||
|
+ # So DATA should be large enough to make this test reliable even with a
|
||||||
|
+ # 64 KiB page configuration.
|
||||||
|
+ DATA = b"x" * (1024 * 17 * 64 + 1)
|
||||||
|
# Reduce socket buffer size to test on relative small data sets.
|
||||||
|
BUF_SIZE = 4 * 1024 # 4 KiB
|
||||||
|
|
@ -1,79 +0,0 @@
|
|||||||
---
|
|
||||||
Lib/test/test_sax.py | 10 +++++-----
|
|
||||||
Lib/test/test_xml_etree.py | 17 ++++++++---------
|
|
||||||
2 files changed, 13 insertions(+), 14 deletions(-)
|
|
||||||
|
|
||||||
--- a/Lib/test/test_sax.py
|
|
||||||
+++ b/Lib/test/test_sax.py
|
|
||||||
@@ -1207,10 +1207,9 @@ class ExpatReaderTest(XmlTestBase):
|
|
||||||
|
|
||||||
self.assertEqual(result.getvalue(), start + b"<doc>text</doc>")
|
|
||||||
|
|
||||||
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
|
|
||||||
+ "Reparse deferral not defined for libexpat < 2.6.0")
|
|
||||||
def test_flush_reparse_deferral_enabled(self):
|
|
||||||
- if pyexpat.version_info < (2, 6, 0):
|
|
||||||
- self.skipTest(f'Expat {pyexpat.version_info} does not support reparse deferral')
|
|
||||||
-
|
|
||||||
result = BytesIO()
|
|
||||||
xmlgen = XMLGenerator(result)
|
|
||||||
parser = create_parser()
|
|
||||||
@@ -1232,6 +1231,8 @@ class ExpatReaderTest(XmlTestBase):
|
|
||||||
|
|
||||||
self.assertEqual(result.getvalue(), start + b"<doc></doc>")
|
|
||||||
|
|
||||||
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
|
|
||||||
+ "Reparse deferral not defined for libexpat < 2.6.0")
|
|
||||||
def test_flush_reparse_deferral_disabled(self):
|
|
||||||
result = BytesIO()
|
|
||||||
xmlgen = XMLGenerator(result)
|
|
||||||
@@ -1241,8 +1242,7 @@ class ExpatReaderTest(XmlTestBase):
|
|
||||||
for chunk in ("<doc", ">"):
|
|
||||||
parser.feed(chunk)
|
|
||||||
|
|
||||||
- if pyexpat.version_info >= (2, 6, 0):
|
|
||||||
- parser._parser.SetReparseDeferralEnabled(False)
|
|
||||||
+ parser._parser.SetReparseDeferralEnabled(False)
|
|
||||||
|
|
||||||
self.assertEqual(result.getvalue(), start) # i.e. no elements started
|
|
||||||
self.assertFalse(parser._parser.GetReparseDeferralEnabled())
|
|
||||||
--- a/Lib/test/test_xml_etree.py
|
|
||||||
+++ b/Lib/test/test_xml_etree.py
|
|
||||||
@@ -1494,11 +1494,9 @@ class XMLPullParserTest(unittest.TestCas
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
ET.XMLPullParser(events=('start', 'end', 'bogus'))
|
|
||||||
|
|
||||||
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
|
|
||||||
+ "Reparse deferral not defined for libexpat < 2.6.0")
|
|
||||||
def test_flush_reparse_deferral_enabled(self):
|
|
||||||
- if pyexpat.version_info < (2, 6, 0):
|
|
||||||
- self.skipTest(f'Expat {pyexpat.version_info} does not '
|
|
||||||
- 'support reparse deferral')
|
|
||||||
-
|
|
||||||
parser = ET.XMLPullParser(events=('start', 'end'))
|
|
||||||
|
|
||||||
for chunk in ("<doc", ">"):
|
|
||||||
@@ -1519,17 +1517,18 @@ class XMLPullParserTest(unittest.TestCas
|
|
||||||
|
|
||||||
self.assert_event_tags(parser, [('end', 'doc')])
|
|
||||||
|
|
||||||
+ @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
|
|
||||||
+ "Reparse deferral not defined for libexpat < 2.6.0")
|
|
||||||
def test_flush_reparse_deferral_disabled(self):
|
|
||||||
parser = ET.XMLPullParser(events=('start', 'end'))
|
|
||||||
|
|
||||||
for chunk in ("<doc", ">"):
|
|
||||||
parser.feed(chunk)
|
|
||||||
|
|
||||||
- if pyexpat.version_info >= (2, 6, 0):
|
|
||||||
- if not ET is pyET:
|
|
||||||
- self.skipTest(f'XMLParser.(Get|Set)ReparseDeferralEnabled '
|
|
||||||
- 'methods not available in C')
|
|
||||||
- parser._parser._parser.SetReparseDeferralEnabled(False)
|
|
||||||
+ if not ET is pyET:
|
|
||||||
+ self.skipTest(f'XMLParser.(Get|Set)ReparseDeferralEnabled '
|
|
||||||
+ 'methods not available in C')
|
|
||||||
+ parser._parser._parser.SetReparseDeferralEnabled(False)
|
|
||||||
|
|
||||||
self.assert_event_tags(parser, []) # i.e. no elements started
|
|
||||||
if ET is pyET:
|
|
@ -1,3 +1,93 @@
|
|||||||
|
-------------------------------------------------------------------
|
||||||
|
Mon Sep 9 20:27:46 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
- Update to 3.8.20:
|
||||||
|
- Tests
|
||||||
|
- gh-112769: The tests now correctly compare zlib version when
|
||||||
|
:const:`zlib.ZLIB_RUNTIME_VERSION` contains non-integer suffixes. For
|
||||||
|
example zlib-ng defines the version as ``1.3.0.zlib-ng``.
|
||||||
|
- gh-117187: Fix XML tests for vanilla Expat <2.6.0.
|
||||||
|
- Security
|
||||||
|
- gh-123678: Upgrade libexpat to 2.6.3
|
||||||
|
- gh-121957: Fixed missing audit events around interactive use of Python,
|
||||||
|
now also properly firing for ``python -i``, as well as for ``python -m
|
||||||
|
asyncio``. The event in question is ``cpython.run_stdin``.
|
||||||
|
- gh-122133: Authenticate the socket connection for the
|
||||||
|
``socket.socketpair()`` fallback on platforms where ``AF_UNIX`` is not
|
||||||
|
available like Windows.
|
||||||
|
Patch by Gregory P. Smith <greg@krypto.org> and Seth Larson
|
||||||
|
<seth@python.org>. Reported by Ellie <el@horse64.org>
|
||||||
|
- gh-121285: Remove backtracking from tarfile header parsing for
|
||||||
|
``hdrcharset``, PAX, and GNU sparse headers
|
||||||
|
(bsc#1230227, CVE-2024-6232).
|
||||||
|
- gh-118486: :func:`os.mkdir` on Windows now accepts *mode* of ``0o700`` to
|
||||||
|
restrict the new directory to the current user. This fixes CVE-2024-4030
|
||||||
|
affecting :func:`tempfile.mkdtemp` in scenarios where the base temporary
|
||||||
|
directory is more permissive than the default.
|
||||||
|
- gh-114572: :meth:`ssl.SSLContext.cert_store_stats` and
|
||||||
|
:meth:`ssl.SSLContext.get_ca_certs` now correctly lock access to the
|
||||||
|
certificate store, when the :class:`ssl.SSLContext` is shared across
|
||||||
|
multiple threads (bsc#1226447, CVE-2024-0397).
|
||||||
|
- gh-116741: Update bundled libexpat to 2.6.2
|
||||||
|
- Library
|
||||||
|
- gh-123270: Applied a more surgical fix for malformed payloads in
|
||||||
|
:class:`zipfile.Path` causing infinite loops (gh-122905) without breaking
|
||||||
|
contents using legitimate characters (bsc#1229704, CVE-2024-8088).
|
||||||
|
- gh-123067: Fix quadratic complexity in parsing ``"``-quoted cookie values
|
||||||
|
with backslashes by :mod:`http.cookies`.
|
||||||
|
- gh-121650: :mod:`email` headers with embedded newlines are now quoted on
|
||||||
|
output. The :mod:`~email.generator` will now refuse to serialize (write)
|
||||||
|
headers that are unsafely folded or delimited; see
|
||||||
|
:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas
|
||||||
|
Bloemsaat and Petr Viktorin in :gh:`121650`; CVE-2024-6923, bsc#1228780).
|
||||||
|
- gh-113171: Fixed various false positives and false negatives in
|
||||||
|
* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
|
||||||
|
* :attr:`ipaddress.IPv4Address.is_global`
|
||||||
|
* :attr:`ipaddress.IPv6Address.is_private`
|
||||||
|
* :attr:`ipaddress.IPv6Address.is_global`
|
||||||
|
Also in the corresponding :class:`ipaddress.IPv4Network` and
|
||||||
|
:class:`ipaddress.IPv6Network` attributes.
|
||||||
|
Fixes bsc#1226448 (CVE-2024-4032).
|
||||||
|
- gh-102988: :func:`email.utils.getaddresses` and
|
||||||
|
:func:`email.utils.parseaddr` now return ``('', '')`` 2-tuples in more
|
||||||
|
situations where invalid email addresses are encountered instead of
|
||||||
|
potentially inaccurate values. Add optional *strict* parameter to these
|
||||||
|
two functions: use ``strict=False`` to get the old behavior, accept
|
||||||
|
malformed inputs. ``getattr(email.utils, 'supports_strict_parsing',
|
||||||
|
False)`` can be use to check if the *strict* paramater is available. Patch
|
||||||
|
by Thomas Dwyer and Victor Stinner to improve the CVE-2023-27043 fix
|
||||||
|
(bsc#1210638).
|
||||||
|
- gh-67693: Fix :func:`urllib.parse.urlunparse` and
|
||||||
|
:func:`urllib.parse.urlunsplit` for URIs with path starting with multiple
|
||||||
|
slashes and no authority. Based on patch by Ashwin Ramaswami.
|
||||||
|
- Core and Builtins
|
||||||
|
- gh-112275: A deadlock involving ``pystate.c``'s ``HEAD_LOCK`` in
|
||||||
|
``posixmodule.c`` at fork is now fixed. Patch by ChuBoning based on
|
||||||
|
previous Python 3.12 fix by Victor Stinner.
|
||||||
|
|
||||||
|
- Remove upstreamed patches:
|
||||||
|
- old-libexpat.patch
|
||||||
|
- CVE-2024-4032-private-IP-addrs.patch
|
||||||
|
- CVE-2024-6923-email-hdr-inject.patch
|
||||||
|
- CVE-2024-6232-cookies-quad-complex.patch
|
||||||
|
- CVE-2023-27043-email-parsing-errors.patch
|
||||||
|
- CVE-2024-8088-inf-loop-zipfile_Path.patch
|
||||||
|
- CVE-2024-0397-memrace_ssl.SSLContext_cert_store.patch
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Thu Sep 5 13:44:48 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
- Add CVE-2024-6232-cookies-quad-complex.patch to avoid quadratic
|
||||||
|
complexity in parsing "-quoted cookie values with backslashes
|
||||||
|
(bsc#1229596, CVE-2024-6232).
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Mon Sep 2 09:44:26 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
- Add gh120226-fix-sendfile-test-kernel-610.patch to avoid
|
||||||
|
failing test_sendfile_close_peer_in_the_middle_of_receiving
|
||||||
|
tests on Linux >= 6.10 (GH-120227).
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Wed Aug 28 16:54:34 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
Wed Aug 28 16:54:34 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@
|
|||||||
%define dynlib() %{sitedir}/lib-dynload/%{1}.cpython-%{abi_tag}-%{archname}-%{_os}%{?_gnu}%{?armsuffix}.so
|
%define dynlib() %{sitedir}/lib-dynload/%{1}.cpython-%{abi_tag}-%{archname}-%{_os}%{?_gnu}%{?armsuffix}.so
|
||||||
%bcond_without profileopt
|
%bcond_without profileopt
|
||||||
Name: %{python_pkg_name}%{psuffix}
|
Name: %{python_pkg_name}%{psuffix}
|
||||||
Version: 3.8.19
|
Version: 3.8.20
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: Python 3 Interpreter
|
Summary: Python 3 Interpreter
|
||||||
License: Python-2.0
|
License: Python-2.0
|
||||||
@ -185,31 +185,16 @@ Patch38: 98437-sphinx.locale._-as-gettext-in-pyspecific.patch
|
|||||||
# PATCH-FIX-UPSTREAM 99366-patch.dict-can-decorate-async.patch bsc#[0-9]+ mcepl@suse.com
|
# PATCH-FIX-UPSTREAM 99366-patch.dict-can-decorate-async.patch bsc#[0-9]+ mcepl@suse.com
|
||||||
# Patch for gh#python/cpython#98086
|
# Patch for gh#python/cpython#98086
|
||||||
Patch41: 99366-patch.dict-can-decorate-async.patch
|
Patch41: 99366-patch.dict-can-decorate-async.patch
|
||||||
# PATCH-FIX-UPSTREAM CVE-2023-27043-email-parsing-errors.patch bsc#1210638 mcepl@suse.com
|
|
||||||
# Detect email address parsing errors and return empty tuple to
|
|
||||||
# indicate the parsing error (old API), from gh#python/cpython!105127
|
|
||||||
Patch42: CVE-2023-27043-email-parsing-errors.patch
|
|
||||||
# PATCH-FIX-UPSTREAM old-libexpat.patch gh#python/cpython#117187 mcepl@suse.com
|
|
||||||
# Make the test suite work with libexpat < 2.6.0
|
|
||||||
Patch43: old-libexpat.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2024-0397-memrace_ssl.SSLContext_cert_store.patch bsc#1226447 mcepl@suse.com
|
|
||||||
# removes memory race condition in ssl.SSLContext certificate store methods
|
|
||||||
Patch44: CVE-2024-0397-memrace_ssl.SSLContext_cert_store.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2024-4032-private-IP-addrs.patch bsc#1226448 mcepl@suse.com
|
|
||||||
# rearrange definition of private v global IP addresses
|
|
||||||
Patch45: CVE-2024-4032-private-IP-addrs.patch
|
|
||||||
# PATCH-FIX-UPSTREAM bso1227999-reproducible-builds.patch bsc#1227999 mcepl@suse.com
|
# PATCH-FIX-UPSTREAM bso1227999-reproducible-builds.patch bsc#1227999 mcepl@suse.com
|
||||||
# reproducibility patches
|
# reproducibility patches
|
||||||
Patch46: bso1227999-reproducible-builds.patch
|
Patch46: bso1227999-reproducible-builds.patch
|
||||||
# PATCH-FIX-UPSTREAM CVE-2024-6923-email-hdr-inject.patch bsc#1228780 mcepl@suse.com
|
|
||||||
# prevent email header injection, patch from gh#python/cpython!122608
|
|
||||||
Patch47: CVE-2024-6923-email-hdr-inject.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2024-5642-OpenSSL-API-buf-overread-NPN.patch bsc#1227233 mcepl@suse.com
|
# PATCH-FIX-UPSTREAM CVE-2024-5642-OpenSSL-API-buf-overread-NPN.patch bsc#1227233 mcepl@suse.com
|
||||||
# Remove for support for anything but OpenSSL 1.1.1 or newer
|
# Remove for support for anything but OpenSSL 1.1.1 or newer
|
||||||
Patch48: CVE-2024-5642-OpenSSL-API-buf-overread-NPN.patch
|
Patch48: CVE-2024-5642-OpenSSL-API-buf-overread-NPN.patch
|
||||||
# PATCH-FIX-UPSTREAM CVE-2024-8088-inf-loop-zipfile_Path.patch bsc#1229704 mcepl@suse.com
|
# PATCH-FIX-UPSTREAM gh120226-fix-sendfile-test-kernel-610.patch gh#python/cpython#120226 mcepl@suse.com
|
||||||
# avoid denial of service in zipfile
|
# Fix test_sendfile_close_peer_in_the_middle_of_receiving on Linux >= 6.10 (GH-120227)
|
||||||
Patch49: CVE-2024-8088-inf-loop-zipfile_Path.patch
|
Patch50: gh120226-fix-sendfile-test-kernel-610.patch
|
||||||
|
|
||||||
BuildRequires: autoconf-archive
|
BuildRequires: autoconf-archive
|
||||||
BuildRequires: automake
|
BuildRequires: automake
|
||||||
BuildRequires: fdupes
|
BuildRequires: fdupes
|
||||||
@ -480,14 +465,9 @@ other applications.
|
|||||||
%patch -p1 -P 37
|
%patch -p1 -P 37
|
||||||
%patch -p1 -P 38
|
%patch -p1 -P 38
|
||||||
%patch -p1 -P 41
|
%patch -p1 -P 41
|
||||||
%patch -p1 -P 42
|
|
||||||
%patch -p1 -P 43
|
|
||||||
%patch -p1 -P 44
|
|
||||||
%patch -p1 -P 45
|
|
||||||
%patch -p1 -P 46
|
%patch -p1 -P 46
|
||||||
%patch -p1 -P 47
|
|
||||||
%patch -p1 -P 48
|
%patch -p1 -P 48
|
||||||
%patch -p1 -P 49
|
%patch -p1 -P 50
|
||||||
|
|
||||||
# drop Autoconf version requirement
|
# drop Autoconf version requirement
|
||||||
sed -i 's/^AC_PREREQ/dnl AC_PREREQ/' configure.ac
|
sed -i 's/^AC_PREREQ/dnl AC_PREREQ/' configure.ac
|
||||||
|
Loading…
Reference in New Issue
Block a user