Compare commits
No commits in common. 'c9-beta' and 'i9c' have entirely different histories.
@ -1 +1 @@
|
|||||||
SOURCES/Python-3.12.5.tar.xz
|
SOURCES/Python-3.12.1.tar.xz
|
||||||
|
@ -1 +1 @@
|
|||||||
d9b83c17a717e1cbd3ab6bd14cfe3e508e6d87b2 SOURCES/Python-3.12.5.tar.xz
|
5b11c58ea58cd6b8e1943c7e9b5f6e0997ca3632 SOURCES/Python-3.12.1.tar.xz
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
From 60d40d7095983e0bc23a103b2050adc519dc7fe3 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Lumir Balhar <lbalhar@redhat.com>
|
|
||||||
Date: Fri, 3 May 2024 14:17:48 +0200
|
|
||||||
Subject: [PATCH] Expect failures in tests not working properly with expat with
|
|
||||||
a fixed CVE in RHEL
|
|
||||||
|
|
||||||
---
|
|
||||||
Lib/test/test_pyexpat.py | 1 +
|
|
||||||
Lib/test/test_sax.py | 1 +
|
|
||||||
Lib/test/test_xml_etree.py | 3 +++
|
|
||||||
3 files changed, 5 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
|
|
||||||
index 43cbd27..27b1502 100644
|
|
||||||
--- a/Lib/test/test_pyexpat.py
|
|
||||||
+++ b/Lib/test/test_pyexpat.py
|
|
||||||
@@ -793,6 +793,7 @@ class ReparseDeferralTest(unittest.TestCase):
|
|
||||||
|
|
||||||
self.assertEqual(started, ['doc'])
|
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
|
||||||
def test_reparse_deferral_disabled(self):
|
|
||||||
started = []
|
|
||||||
|
|
||||||
diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py
|
|
||||||
index 9b3014a..646c92d 100644
|
|
||||||
--- a/Lib/test/test_sax.py
|
|
||||||
+++ b/Lib/test/test_sax.py
|
|
||||||
@@ -1240,6 +1240,7 @@ class ExpatReaderTest(XmlTestBase):
|
|
||||||
|
|
||||||
self.assertEqual(result.getvalue(), start + b"<doc></doc>")
|
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
|
||||||
def test_flush_reparse_deferral_disabled(self):
|
|
||||||
result = BytesIO()
|
|
||||||
xmlgen = XMLGenerator(result)
|
|
||||||
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
|
|
||||||
index 9c382d1..62f2871 100644
|
|
||||||
--- a/Lib/test/test_xml_etree.py
|
|
||||||
+++ b/Lib/test/test_xml_etree.py
|
|
||||||
@@ -1424,9 +1424,11 @@ class XMLPullParserTest(unittest.TestCase):
|
|
||||||
self.assert_event_tags(parser, [('end', 'root')])
|
|
||||||
self.assertIsNone(parser.close())
|
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
|
||||||
def test_simple_xml_chunk_1(self):
|
|
||||||
self.test_simple_xml(chunk_size=1, flush=True)
|
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
|
||||||
def test_simple_xml_chunk_5(self):
|
|
||||||
self.test_simple_xml(chunk_size=5, flush=True)
|
|
||||||
|
|
||||||
@@ -1651,6 +1653,7 @@ class XMLPullParserTest(unittest.TestCase):
|
|
||||||
|
|
||||||
self.assert_event_tags(parser, [('end', 'doc')])
|
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
|
||||||
def test_flush_reparse_deferral_disabled(self):
|
|
||||||
parser = ET.XMLPullParser(events=('start', 'end'))
|
|
||||||
|
|
||||||
--
|
|
||||||
2.44.0
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
Date: Sun, 11 Feb 2024 12:08:39 +0200
|
||||||
|
Subject: [PATCH] 00422: gh-115133: Fix tests for XMLPullParser with Expat
|
||||||
|
2.6.0
|
||||||
|
|
||||||
|
Feeding the parser by too small chunks defers parsing to prevent
|
||||||
|
CVE-2023-52425. Future versions of Expat may be more reactive.
|
||||||
|
|
||||||
|
(cherry picked from commit 4a08e7b3431cd32a0daf22a33421cd3035343dc4)
|
||||||
|
---
|
||||||
|
Lib/test/test_xml_etree.py | 58 ++++++++++++-------
|
||||||
|
...-02-08-14-21-28.gh-issue-115133.ycl4ko.rst | 2 +
|
||||||
|
2 files changed, 38 insertions(+), 22 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2024-02-08-14-21-28.gh-issue-115133.ycl4ko.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
|
||||||
|
index b50898f1d1..6fb888cb21 100644
|
||||||
|
--- a/Lib/test/test_xml_etree.py
|
||||||
|
+++ b/Lib/test/test_xml_etree.py
|
||||||
|
@@ -1400,28 +1400,37 @@ class XMLPullParserTest(unittest.TestCase):
|
||||||
|
self.assertEqual([(action, elem.tag) for action, elem in events],
|
||||||
|
expected)
|
||||||
|
|
||||||
|
- def test_simple_xml(self):
|
||||||
|
- for chunk_size in (None, 1, 5):
|
||||||
|
- with self.subTest(chunk_size=chunk_size):
|
||||||
|
- parser = ET.XMLPullParser()
|
||||||
|
- self.assert_event_tags(parser, [])
|
||||||
|
- self._feed(parser, "<!-- comment -->\n", chunk_size)
|
||||||
|
- self.assert_event_tags(parser, [])
|
||||||
|
- self._feed(parser,
|
||||||
|
- "<root>\n <element key='value'>text</element",
|
||||||
|
- chunk_size)
|
||||||
|
- self.assert_event_tags(parser, [])
|
||||||
|
- self._feed(parser, ">\n", chunk_size)
|
||||||
|
- self.assert_event_tags(parser, [('end', 'element')])
|
||||||
|
- self._feed(parser, "<element>text</element>tail\n", chunk_size)
|
||||||
|
- self._feed(parser, "<empty-element/>\n", chunk_size)
|
||||||
|
- self.assert_event_tags(parser, [
|
||||||
|
- ('end', 'element'),
|
||||||
|
- ('end', 'empty-element'),
|
||||||
|
- ])
|
||||||
|
- self._feed(parser, "</root>\n", chunk_size)
|
||||||
|
- self.assert_event_tags(parser, [('end', 'root')])
|
||||||
|
- self.assertIsNone(parser.close())
|
||||||
|
+ def test_simple_xml(self, chunk_size=None):
|
||||||
|
+ parser = ET.XMLPullParser()
|
||||||
|
+ self.assert_event_tags(parser, [])
|
||||||
|
+ self._feed(parser, "<!-- comment -->\n", chunk_size)
|
||||||
|
+ self.assert_event_tags(parser, [])
|
||||||
|
+ self._feed(parser,
|
||||||
|
+ "<root>\n <element key='value'>text</element",
|
||||||
|
+ chunk_size)
|
||||||
|
+ self.assert_event_tags(parser, [])
|
||||||
|
+ self._feed(parser, ">\n", chunk_size)
|
||||||
|
+ self.assert_event_tags(parser, [('end', 'element')])
|
||||||
|
+ self._feed(parser, "<element>text</element>tail\n", chunk_size)
|
||||||
|
+ self._feed(parser, "<empty-element/>\n", chunk_size)
|
||||||
|
+ self.assert_event_tags(parser, [
|
||||||
|
+ ('end', 'element'),
|
||||||
|
+ ('end', 'empty-element'),
|
||||||
|
+ ])
|
||||||
|
+ self._feed(parser, "</root>\n", chunk_size)
|
||||||
|
+ self.assert_event_tags(parser, [('end', 'root')])
|
||||||
|
+ self.assertIsNone(parser.close())
|
||||||
|
+
|
||||||
|
+ @unittest.expectedFailure
|
||||||
|
+ def test_simple_xml_chunk_1(self):
|
||||||
|
+ self.test_simple_xml(chunk_size=1)
|
||||||
|
+
|
||||||
|
+ @unittest.expectedFailure
|
||||||
|
+ def test_simple_xml_chunk_5(self):
|
||||||
|
+ self.test_simple_xml(chunk_size=5)
|
||||||
|
+
|
||||||
|
+ def test_simple_xml_chunk_22(self):
|
||||||
|
+ self.test_simple_xml(chunk_size=22)
|
||||||
|
|
||||||
|
def test_feed_while_iterating(self):
|
||||||
|
parser = ET.XMLPullParser()
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2024-02-08-14-21-28.gh-issue-115133.ycl4ko.rst b/Misc/NEWS.d/next/Library/2024-02-08-14-21-28.gh-issue-115133.ycl4ko.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000..6f1015235c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Library/2024-02-08-14-21-28.gh-issue-115133.ycl4ko.rst
|
||||||
|
@@ -0,0 +1,2 @@
|
||||||
|
+Fix tests for :class:`~xml.etree.ElementTree.XMLPullParser` with Expat
|
||||||
|
+2.6.0.
|
@ -0,0 +1,345 @@
|
|||||||
|
From 7a25b2f511054dd2011308275bb24e914e1977af Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <encukou@gmail.com>
|
||||||
|
Date: Tue, 6 Aug 2024 19:07:19 +0200
|
||||||
|
Subject: [PATCH] gh-121650: Encode newlines in headers, and verify headers are
|
||||||
|
sound (GH-122233) (#122599)
|
||||||
|
|
||||||
|
* gh-121650: Encode newlines in headers, and verify headers are sound (GH-122233)
|
||||||
|
|
||||||
|
- Encode header parts that contain newlines
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
|
||||||
|
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
|
||||||
|
|
||||||
|
* Document changes as made in 3.12.5
|
||||||
|
---
|
||||||
|
Doc/library/email.errors.rst | 7 +++
|
||||||
|
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 ++++++++
|
||||||
|
...-07-27-16-10-41.gh-issue-121650.nf6oc9.rst | 5 ++
|
||||||
|
9 files changed, 151 insertions(+), 4 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
|
||||||
|
|
||||||
|
diff --git a/Doc/library/email.errors.rst b/Doc/library/email.errors.rst
|
||||||
|
index 56aea65..27b0481 100644
|
||||||
|
--- a/Doc/library/email.errors.rst
|
||||||
|
+++ b/Doc/library/email.errors.rst
|
||||||
|
@@ -58,6 +58,13 @@ 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.
|
||||||
|
+
|
||||||
|
+
|
||||||
|
.. exception:: MessageDefect()
|
||||||
|
|
||||||
|
This is the base class for all defects found when parsing email messages.
|
||||||
|
diff --git a/Doc/library/email.policy.rst b/Doc/library/email.policy.rst
|
||||||
|
index fd47dd0..6ec6e4d 100644
|
||||||
|
--- a/Doc/library/email.policy.rst
|
||||||
|
+++ b/Doc/library/email.policy.rst
|
||||||
|
@@ -230,6 +230,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.12.5
|
||||||
|
+
|
||||||
|
+
|
||||||
|
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 0d6bd81..362edc5 100644
|
||||||
|
--- 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'\"')+'"'
|
||||||
|
@@ -2776,9 +2778,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 c9cbadd..d1f4821 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 3ad0056..02aa5ec 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 7ccbe10..ea87ad2 100644
|
||||||
|
--- 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]')
|
||||||
|
|
||||||
|
|
||||||
|
class Generator:
|
||||||
|
@@ -222,7 +224,16 @@ 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}')
|
||||||
|
+ 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 89e7ede..d29400f 100644
|
||||||
|
--- 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, 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 e87c275..ff1ddf7 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 @@ 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).
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst b/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..83dd28d
|
||||||
|
--- /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`.)
|
||||||
|
--
|
||||||
|
2.45.2
|
||||||
|
|
@ -0,0 +1,245 @@
|
|||||||
|
From 4eaf4891c12589e3c7bdad5f5b076e4c8392dd06 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Sun, 1 Sep 2024 00:35:24 +0200
|
||||||
|
Subject: [PATCH] [3.12] gh-121285: Remove backtracking when parsing tarfile
|
||||||
|
headers (GH-121286) (GH-123543)
|
||||||
|
|
||||||
|
gh-121285: Remove backtracking when parsing tarfile headers (GH-121286)
|
||||||
|
|
||||||
|
* Remove backtracking when parsing tarfile headers
|
||||||
|
* Rewrite PAX header parsing to be stricter
|
||||||
|
* Optimize parsing of GNU extended sparse headers v0.0
|
||||||
|
|
||||||
|
(cherry picked from commit 34ddb64d088dd7ccc321f6103d23153256caa5d4)
|
||||||
|
|
||||||
|
Co-authored-by: Seth Michael Larson <seth@python.org>
|
||||||
|
Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru>
|
||||||
|
Co-authored-by: Gregory P. Smith <greg@krypto.org>
|
||||||
|
---
|
||||||
|
Lib/tarfile.py | 103 ++++++++++++------
|
||||||
|
Lib/test/test_tarfile.py | 42 +++++++
|
||||||
|
...-07-02-13-39-20.gh-issue-121285.hrl-yI.rst | 2 +
|
||||||
|
3 files changed, 112 insertions(+), 35 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
|
||||||
|
index e1487e3864d44b..0a0f31eca06c04 100755
|
||||||
|
--- a/Lib/tarfile.py
|
||||||
|
+++ b/Lib/tarfile.py
|
||||||
|
@@ -843,6 +843,9 @@ def data_filter(member, dest_path):
|
||||||
|
# Sentinel for replace() defaults, meaning "don't change the attribute"
|
||||||
|
_KEEP = object()
|
||||||
|
|
||||||
|
+# Header length is digits followed by a space.
|
||||||
|
+_header_length_prefix_re = re.compile(br"([0-9]{1,20}) ")
|
||||||
|
+
|
||||||
|
class TarInfo(object):
|
||||||
|
"""Informational class which holds the details about an
|
||||||
|
archive member given by a tar header block.
|
||||||
|
@@ -1412,37 +1415,59 @@ def _proc_pax(self, tarfile):
|
||||||
|
else:
|
||||||
|
pax_headers = tarfile.pax_headers.copy()
|
||||||
|
|
||||||
|
- # Check if the pax header contains a hdrcharset field. This tells us
|
||||||
|
- # the encoding of the path, linkpath, uname and gname fields. Normally,
|
||||||
|
- # these fields are UTF-8 encoded but since POSIX.1-2008 tar
|
||||||
|
- # implementations are allowed to store them as raw binary strings if
|
||||||
|
- # the translation to UTF-8 fails.
|
||||||
|
- match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf)
|
||||||
|
- if match is not None:
|
||||||
|
- pax_headers["hdrcharset"] = match.group(1).decode("utf-8")
|
||||||
|
-
|
||||||
|
- # For the time being, we don't care about anything other than "BINARY".
|
||||||
|
- # The only other value that is currently allowed by the standard is
|
||||||
|
- # "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
|
||||||
|
- hdrcharset = pax_headers.get("hdrcharset")
|
||||||
|
- if hdrcharset == "BINARY":
|
||||||
|
- encoding = tarfile.encoding
|
||||||
|
- else:
|
||||||
|
- encoding = "utf-8"
|
||||||
|
-
|
||||||
|
# Parse pax header information. A record looks like that:
|
||||||
|
# "%d %s=%s\n" % (length, keyword, value). length is the size
|
||||||
|
# of the complete record including the length field itself and
|
||||||
|
- # the newline. keyword and value are both UTF-8 encoded strings.
|
||||||
|
- regex = re.compile(br"(\d+) ([^=]+)=")
|
||||||
|
+ # the newline.
|
||||||
|
pos = 0
|
||||||
|
- while match := regex.match(buf, pos):
|
||||||
|
- length, keyword = match.groups()
|
||||||
|
- length = int(length)
|
||||||
|
- if length == 0:
|
||||||
|
+ encoding = None
|
||||||
|
+ raw_headers = []
|
||||||
|
+ while len(buf) > pos and buf[pos] != 0x00:
|
||||||
|
+ if not (match := _header_length_prefix_re.match(buf, pos)):
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
+ try:
|
||||||
|
+ length = int(match.group(1))
|
||||||
|
+ except ValueError:
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
+ # Headers must be at least 5 bytes, shortest being '5 x=\n'.
|
||||||
|
+ # Value is allowed to be empty.
|
||||||
|
+ if length < 5:
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
+ if pos + length > len(buf):
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
+
|
||||||
|
+ header_value_end_offset = match.start(1) + length - 1 # Last byte of the header
|
||||||
|
+ keyword_and_value = buf[match.end(1) + 1:header_value_end_offset]
|
||||||
|
+ raw_keyword, equals, raw_value = keyword_and_value.partition(b"=")
|
||||||
|
+
|
||||||
|
+ # Check the framing of the header. The last character must be '\n' (0x0A)
|
||||||
|
+ if not raw_keyword or equals != b"=" or buf[header_value_end_offset] != 0x0A:
|
||||||
|
raise InvalidHeaderError("invalid header")
|
||||||
|
- value = buf[match.end(2) + 1:match.start(1) + length - 1]
|
||||||
|
+ raw_headers.append((length, raw_keyword, raw_value))
|
||||||
|
+
|
||||||
|
+ # Check if the pax header contains a hdrcharset field. This tells us
|
||||||
|
+ # the encoding of the path, linkpath, uname and gname fields. Normally,
|
||||||
|
+ # these fields are UTF-8 encoded but since POSIX.1-2008 tar
|
||||||
|
+ # implementations are allowed to store them as raw binary strings if
|
||||||
|
+ # the translation to UTF-8 fails. For the time being, we don't care about
|
||||||
|
+ # anything other than "BINARY". The only other value that is currently
|
||||||
|
+ # allowed by the standard is "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
|
||||||
|
+ # Note that we only follow the initial 'hdrcharset' setting to preserve
|
||||||
|
+ # the initial behavior of the 'tarfile' module.
|
||||||
|
+ if raw_keyword == b"hdrcharset" and encoding is None:
|
||||||
|
+ if raw_value == b"BINARY":
|
||||||
|
+ encoding = tarfile.encoding
|
||||||
|
+ else: # This branch ensures only the first 'hdrcharset' header is used.
|
||||||
|
+ encoding = "utf-8"
|
||||||
|
|
||||||
|
+ pos += length
|
||||||
|
+
|
||||||
|
+ # If no explicit hdrcharset is set, we use UTF-8 as a default.
|
||||||
|
+ if encoding is None:
|
||||||
|
+ encoding = "utf-8"
|
||||||
|
+
|
||||||
|
+ # After parsing the raw headers we can decode them to text.
|
||||||
|
+ for length, raw_keyword, raw_value in raw_headers:
|
||||||
|
# Normally, we could just use "utf-8" as the encoding and "strict"
|
||||||
|
# as the error handler, but we better not take the risk. For
|
||||||
|
# example, GNU tar <= 1.23 is known to store filenames it cannot
|
||||||
|
@@ -1450,17 +1475,16 @@ def _proc_pax(self, tarfile):
|
||||||
|
# hdrcharset=BINARY header).
|
||||||
|
# We first try the strict standard encoding, and if that fails we
|
||||||
|
# fall back on the user's encoding and error handler.
|
||||||
|
- keyword = self._decode_pax_field(keyword, "utf-8", "utf-8",
|
||||||
|
+ keyword = self._decode_pax_field(raw_keyword, "utf-8", "utf-8",
|
||||||
|
tarfile.errors)
|
||||||
|
if keyword in PAX_NAME_FIELDS:
|
||||||
|
- value = self._decode_pax_field(value, encoding, tarfile.encoding,
|
||||||
|
+ value = self._decode_pax_field(raw_value, encoding, tarfile.encoding,
|
||||||
|
tarfile.errors)
|
||||||
|
else:
|
||||||
|
- value = self._decode_pax_field(value, "utf-8", "utf-8",
|
||||||
|
+ value = self._decode_pax_field(raw_value, "utf-8", "utf-8",
|
||||||
|
tarfile.errors)
|
||||||
|
|
||||||
|
pax_headers[keyword] = value
|
||||||
|
- pos += length
|
||||||
|
|
||||||
|
# Fetch the next header.
|
||||||
|
try:
|
||||||
|
@@ -1475,7 +1499,7 @@ def _proc_pax(self, tarfile):
|
||||||
|
|
||||||
|
elif "GNU.sparse.size" in pax_headers:
|
||||||
|
# GNU extended sparse format version 0.0.
|
||||||
|
- self._proc_gnusparse_00(next, pax_headers, buf)
|
||||||
|
+ self._proc_gnusparse_00(next, raw_headers)
|
||||||
|
|
||||||
|
elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0":
|
||||||
|
# GNU extended sparse format version 1.0.
|
||||||
|
@@ -1497,15 +1521,24 @@ def _proc_pax(self, tarfile):
|
||||||
|
|
||||||
|
return next
|
||||||
|
|
||||||
|
- def _proc_gnusparse_00(self, next, pax_headers, buf):
|
||||||
|
+ def _proc_gnusparse_00(self, next, raw_headers):
|
||||||
|
"""Process a GNU tar extended sparse header, version 0.0.
|
||||||
|
"""
|
||||||
|
offsets = []
|
||||||
|
- for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf):
|
||||||
|
- offsets.append(int(match.group(1)))
|
||||||
|
numbytes = []
|
||||||
|
- for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf):
|
||||||
|
- numbytes.append(int(match.group(1)))
|
||||||
|
+ for _, keyword, value in raw_headers:
|
||||||
|
+ if keyword == b"GNU.sparse.offset":
|
||||||
|
+ try:
|
||||||
|
+ offsets.append(int(value.decode()))
|
||||||
|
+ except ValueError:
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
+
|
||||||
|
+ elif keyword == b"GNU.sparse.numbytes":
|
||||||
|
+ try:
|
||||||
|
+ numbytes.append(int(value.decode()))
|
||||||
|
+ except ValueError:
|
||||||
|
+ raise InvalidHeaderError("invalid header")
|
||||||
|
+
|
||||||
|
next.sparse = list(zip(offsets, numbytes))
|
||||||
|
|
||||||
|
def _proc_gnusparse_01(self, next, pax_headers):
|
||||||
|
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
|
||||||
|
index 3fbd25e742b181..e28d0311826e2b 100644
|
||||||
|
--- a/Lib/test/test_tarfile.py
|
||||||
|
+++ b/Lib/test/test_tarfile.py
|
||||||
|
@@ -1237,6 +1237,48 @@ def test_pax_number_fields(self):
|
||||||
|
finally:
|
||||||
|
tar.close()
|
||||||
|
|
||||||
|
+ def test_pax_header_bad_formats(self):
|
||||||
|
+ # The fields from the pax header have priority over the
|
||||||
|
+ # TarInfo.
|
||||||
|
+ pax_header_replacements = (
|
||||||
|
+ b" foo=bar\n",
|
||||||
|
+ b"0 \n",
|
||||||
|
+ b"1 \n",
|
||||||
|
+ b"2 \n",
|
||||||
|
+ b"3 =\n",
|
||||||
|
+ b"4 =a\n",
|
||||||
|
+ b"1000000 foo=bar\n",
|
||||||
|
+ b"0 foo=bar\n",
|
||||||
|
+ b"-12 foo=bar\n",
|
||||||
|
+ b"000000000000000000000000036 foo=bar\n",
|
||||||
|
+ )
|
||||||
|
+ pax_headers = {"foo": "bar"}
|
||||||
|
+
|
||||||
|
+ for replacement in pax_header_replacements:
|
||||||
|
+ with self.subTest(header=replacement):
|
||||||
|
+ tar = tarfile.open(tmpname, "w", format=tarfile.PAX_FORMAT,
|
||||||
|
+ encoding="iso8859-1")
|
||||||
|
+ try:
|
||||||
|
+ t = tarfile.TarInfo()
|
||||||
|
+ t.name = "pax" # non-ASCII
|
||||||
|
+ t.uid = 1
|
||||||
|
+ t.pax_headers = pax_headers
|
||||||
|
+ tar.addfile(t)
|
||||||
|
+ finally:
|
||||||
|
+ tar.close()
|
||||||
|
+
|
||||||
|
+ with open(tmpname, "rb") as f:
|
||||||
|
+ data = f.read()
|
||||||
|
+ self.assertIn(b"11 foo=bar\n", data)
|
||||||
|
+ data = data.replace(b"11 foo=bar\n", replacement)
|
||||||
|
+
|
||||||
|
+ with open(tmpname, "wb") as f:
|
||||||
|
+ f.truncate()
|
||||||
|
+ f.write(data)
|
||||||
|
+
|
||||||
|
+ with self.assertRaisesRegex(tarfile.ReadError, r"method tar: ReadError\('invalid header'\)"):
|
||||||
|
+ tarfile.open(tmpname, encoding="iso8859-1")
|
||||||
|
+
|
||||||
|
|
||||||
|
class WriteTestBase(TarTest):
|
||||||
|
# Put all write tests in here that are supposed to be tested
|
||||||
|
diff --git a/Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst b/Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000000..81f918bfe2b255
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2024-07-02-13-39-20.gh-issue-121285.hrl-yI.rst
|
||||||
|
@@ -0,0 +1,2 @@
|
||||||
|
+Remove backtracking from tarfile header parsing for ``hdrcharset``, PAX, and
|
||||||
|
+GNU sparse headers.
|
@ -0,0 +1,18 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmVyMspfFIAAAAAALgAo
|
||||||
|
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
||||||
|
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
||||||
|
YwWv5w/+JlGtfy+x+6mtauH1uOkt7n9PMQou1LcthDs5s41wuwjO7RbwnmJD6aDk
|
||||||
|
DqwLHheoq6Kjbl6PF1kG2T8ZbHkMudhnc5yH4eQG52IGNQ6evilxoC6AyhVg8ANi
|
||||||
|
+u6Juh9r2Hjz/LDWFB4hzwcOBKy0jYw98+A0uMvpPd2bmdFMBLQE0GTZCdrRsGYs
|
||||||
|
q0oysUX7uCJBfINp7XwiVGAK/6ma0nrr0A1ho6LCau+VGkDnJZdKZgIMyyxp6qL1
|
||||||
|
7tMjb3LUpV3FWp57L2za59TaayApNf5BlanC+de6oKEhEJ8oEFyWxOx2GmXHZwch
|
||||||
|
ucj7Z1dxuI7fjNVkEvZ+JuheLGtB9mAmUZslXgUJf5wo49bCo9E4/ZlIFQk7VJR3
|
||||||
|
Bm9VlQb5mMydB8QJbMy/BpgNjgKmEvBTnir37prJpUV/TL1YZT0eZ5JxCnlUIL/F
|
||||||
|
6cOzAE3zHPnvHcyHhKV3q5CoONdBtB3RWgS66m4eMneuWoNKaoEbO5IDxtKvCd1J
|
||||||
|
AKLmzCB0/KCWVUIYBTfJ8ytBVQA0Z2w8CZ7SC8asX4DocDCvxim1sQg5s8c4mzh+
|
||||||
|
1JVbyqqEmf9m74Mqby0vICC6UVvgaPyiOxTphtRXLIYHUscLVn5+586RMYnM9nP4
|
||||||
|
nEK+H/fq6Rcp1XEtIPzCG4IPUAYnuDLjbGQegltpKV/SAYn+DGg=
|
||||||
|
=dCpy
|
||||||
|
-----END PGP SIGNATURE-----
|
@ -1,18 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmayiFtfFIAAAAAALgAo
|
|
||||||
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
|
||||||
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
|
||||||
YwUr4g//VyVs9tvbtiSp8pGe8f1gYErEw54r124sL/CBuNii8Irts1j5ymGxcm+l
|
|
||||||
hshPK5UlqRnhd5dCJWFTvLTXa5Ko2R1L3JyyxfGd1hmDuMhrWsDHijI0R7L/mGM5
|
|
||||||
6X2LTaadBVNvk8HaNKvR8SEWvo68rdnOuYElFA9ir7uqwjO26ZWz9FfH80YDGwo8
|
|
||||||
Blef2NYw8rNhiaZMFV0HYV7D+YyUAZnFNfW8M7Fd4oskUyj1tD9J89T9FFLYN09d
|
|
||||||
BcCIf+EdiEfqRpKxH89bW2g52kDrm4jYGONtpyF8eruyS3YwYSbvbuWioBYKmlxC
|
|
||||||
s51mieXz6G325GTZnmPxLek3ywPv6Gil9y0wH3fIr2BsWsmXust4LBpjDGt56Fy6
|
|
||||||
seokGBg8xzsBSk3iEqNoFmNsy/QOiuCcDejX4XqBDNodOlETQPJb07TkTI2iOmg9
|
|
||||||
NG4Atiz1HvGVxK68UuK9IIcNHyaWUmH8h4VQFGvc6KV6feP5Nm21Y12PZ5XIqJBO
|
|
||||||
Y8M/VJIJ5koaNPQfnBbbI5YBkUr4BVpIXIpY5LM/L5sUo2C3R7hMi0VGK88HGfSQ
|
|
||||||
KV4JmZgf6RMBNmrWY12sryS1QQ6q3P110GTUGQWB3sxxNbhmfcrK+4viqHc83yDz
|
|
||||||
ifmk33HuqaQGU7OzUMHeNcoCJIPo3H1FpoHOn9wLLCtA1pT+as4=
|
|
||||||
=t0Rk
|
|
||||||
-----END PGP SIGNATURE-----
|
|
Loading…
Reference in new issue