Compare commits
No commits in common. 'c9-beta' and 'c9' have entirely different histories.
@ -1 +1 @@
|
|||||||
SOURCES/Python-3.9.19.tar.xz
|
SOURCES/Python-3.9.18.tar.xz
|
||||||
|
@ -1 +1 @@
|
|||||||
57d08ec0b329a78923b486abae906d4fa12fadb7 SOURCES/Python-3.9.19.tar.xz
|
abe4a20dcc11798495b17611ef9f8f33d6975722 SOURCES/Python-3.9.18.tar.xz
|
||||||
|
@ -1,63 +1,77 @@
|
|||||||
From 60d40d7095983e0bc23a103b2050adc519dc7fe3 Mon Sep 17 00:00:00 2001
|
From c9364e8727ea2426519a74593ab03ebcb0da72b8 Mon Sep 17 00:00:00 2001
|
||||||
From: Lumir Balhar <lbalhar@redhat.com>
|
From: Lumir Balhar <lbalhar@redhat.com>
|
||||||
Date: Fri, 3 May 2024 14:17:48 +0200
|
Date: Fri, 3 May 2024 14:17:48 +0200
|
||||||
Subject: [PATCH] Expect failures in tests not working properly with expat with
|
Subject: [PATCH] Expect failures in tests not working properly with expat with
|
||||||
a fixed CVE in RHEL
|
a fixed CVE in RHEL
|
||||||
|
|
||||||
---
|
---
|
||||||
Lib/test/test_pyexpat.py | 1 +
|
Lib/test/test_xml_etree.py | 53 ++++++++++++++++++++++----------------
|
||||||
Lib/test/test_sax.py | 1 +
|
1 file changed, 31 insertions(+), 22 deletions(-)
|
||||||
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
|
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
|
||||||
index 9c382d1..62f2871 100644
|
index 7c346f2..24e0bb8 100644
|
||||||
--- a/Lib/test/test_xml_etree.py
|
--- a/Lib/test/test_xml_etree.py
|
||||||
+++ b/Lib/test/test_xml_etree.py
|
+++ b/Lib/test/test_xml_etree.py
|
||||||
@@ -1424,9 +1424,11 @@ class XMLPullParserTest(unittest.TestCase):
|
@@ -1391,28 +1391,37 @@ class XMLPullParserTest(unittest.TestCase):
|
||||||
self.assert_event_tags(parser, [('end', 'root')])
|
self.assertEqual([(action, elem.tag) for action, elem in events],
|
||||||
self.assertIsNone(parser.close())
|
expected)
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
|
||||||
def test_simple_xml_chunk_1(self):
|
|
||||||
self.test_simple_xml(chunk_size=1, flush=True)
|
|
||||||
|
|
||||||
|
- 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
|
+ @unittest.expectedFailure
|
||||||
def test_simple_xml_chunk_5(self):
|
+ def test_simple_xml_chunk_1(self):
|
||||||
self.test_simple_xml(chunk_size=5, flush=True)
|
+ self.test_simple_xml(chunk_size=1)
|
||||||
|
+
|
||||||
@@ -1651,6 +1653,7 @@ class XMLPullParserTest(unittest.TestCase):
|
|
||||||
|
|
||||||
self.assert_event_tags(parser, [('end', 'doc')])
|
|
||||||
|
|
||||||
+ @unittest.expectedFailure
|
+ @unittest.expectedFailure
|
||||||
def test_flush_reparse_deferral_disabled(self):
|
+ def test_simple_xml_chunk_5(self):
|
||||||
parser = ET.XMLPullParser(events=('start', 'end'))
|
+ 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()
|
||||||
--
|
--
|
||||||
2.44.0
|
2.45.0
|
||||||
|
|
||||||
|
@ -0,0 +1,211 @@
|
|||||||
|
From d54e22a669ae6e987199bb5d2c69bb5a46b0083b Mon Sep 17 00:00:00 2001
|
||||||
|
From: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
Date: Wed, 17 Jan 2024 15:47:47 +0200
|
||||||
|
Subject: [PATCH] [3.9] gh-91133: tempfile.TemporaryDirectory: fix symlink bug
|
||||||
|
in cleanup (GH-99930) (GH-112842)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
(cherry picked from commit 81c16cd94ec38d61aa478b9a452436dc3b1b524d)
|
||||||
|
|
||||||
|
Co-authored-by: Søren Løvborg <sorenl@unity3d.com>
|
||||||
|
---
|
||||||
|
Lib/tempfile.py | 27 ++--
|
||||||
|
Lib/test/test_tempfile.py | 117 +++++++++++++++++-
|
||||||
|
...2-12-01-16-57-44.gh-issue-91133.LKMVCV.rst | 2 +
|
||||||
|
3 files changed, 136 insertions(+), 10 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/tempfile.py b/Lib/tempfile.py
|
||||||
|
index eafce6f25b6fb2..59a628a1744685 100644
|
||||||
|
--- a/Lib/tempfile.py
|
||||||
|
+++ b/Lib/tempfile.py
|
||||||
|
@@ -268,6 +268,22 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
|
||||||
|
raise FileExistsError(_errno.EEXIST,
|
||||||
|
"No usable temporary file name found")
|
||||||
|
|
||||||
|
+def _dont_follow_symlinks(func, path, *args):
|
||||||
|
+ # Pass follow_symlinks=False, unless not supported on this platform.
|
||||||
|
+ if func in _os.supports_follow_symlinks:
|
||||||
|
+ func(path, *args, follow_symlinks=False)
|
||||||
|
+ elif _os.name == 'nt' or not _os.path.islink(path):
|
||||||
|
+ func(path, *args)
|
||||||
|
+
|
||||||
|
+def _resetperms(path):
|
||||||
|
+ try:
|
||||||
|
+ chflags = _os.chflags
|
||||||
|
+ except AttributeError:
|
||||||
|
+ pass
|
||||||
|
+ else:
|
||||||
|
+ _dont_follow_symlinks(chflags, path, 0)
|
||||||
|
+ _dont_follow_symlinks(_os.chmod, path, 0o700)
|
||||||
|
+
|
||||||
|
|
||||||
|
# User visible interfaces.
|
||||||
|
|
||||||
|
@@ -789,17 +805,10 @@ def __init__(self, suffix=None, prefix=None, dir=None):
|
||||||
|
def _rmtree(cls, name):
|
||||||
|
def onerror(func, path, exc_info):
|
||||||
|
if issubclass(exc_info[0], PermissionError):
|
||||||
|
- def resetperms(path):
|
||||||
|
- try:
|
||||||
|
- _os.chflags(path, 0)
|
||||||
|
- except AttributeError:
|
||||||
|
- pass
|
||||||
|
- _os.chmod(path, 0o700)
|
||||||
|
-
|
||||||
|
try:
|
||||||
|
if path != name:
|
||||||
|
- resetperms(_os.path.dirname(path))
|
||||||
|
- resetperms(path)
|
||||||
|
+ _resetperms(_os.path.dirname(path))
|
||||||
|
+ _resetperms(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_os.unlink(path)
|
||||||
|
diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py
|
||||||
|
index 8ad1bb98e8e899..571263d9c957d7 100644
|
||||||
|
--- a/Lib/test/test_tempfile.py
|
||||||
|
+++ b/Lib/test/test_tempfile.py
|
||||||
|
@@ -1394,6 +1394,103 @@ def test_cleanup_with_symlink_to_a_directory(self):
|
||||||
|
"were deleted")
|
||||||
|
d2.cleanup()
|
||||||
|
|
||||||
|
+ @support.skip_unless_symlink
|
||||||
|
+ def test_cleanup_with_symlink_modes(self):
|
||||||
|
+ # cleanup() should not follow symlinks when fixing mode bits (#91133)
|
||||||
|
+ with self.do_create(recurse=0) as d2:
|
||||||
|
+ file1 = os.path.join(d2, 'file1')
|
||||||
|
+ open(file1, 'wb').close()
|
||||||
|
+ dir1 = os.path.join(d2, 'dir1')
|
||||||
|
+ os.mkdir(dir1)
|
||||||
|
+ for mode in range(8):
|
||||||
|
+ mode <<= 6
|
||||||
|
+ with self.subTest(mode=format(mode, '03o')):
|
||||||
|
+ def test(target, target_is_directory):
|
||||||
|
+ d1 = self.do_create(recurse=0)
|
||||||
|
+ symlink = os.path.join(d1.name, 'symlink')
|
||||||
|
+ os.symlink(target, symlink,
|
||||||
|
+ target_is_directory=target_is_directory)
|
||||||
|
+ try:
|
||||||
|
+ os.chmod(symlink, mode, follow_symlinks=False)
|
||||||
|
+ except NotImplementedError:
|
||||||
|
+ pass
|
||||||
|
+ try:
|
||||||
|
+ os.chmod(symlink, mode)
|
||||||
|
+ except FileNotFoundError:
|
||||||
|
+ pass
|
||||||
|
+ os.chmod(d1.name, mode)
|
||||||
|
+ d1.cleanup()
|
||||||
|
+ self.assertFalse(os.path.exists(d1.name))
|
||||||
|
+
|
||||||
|
+ with self.subTest('nonexisting file'):
|
||||||
|
+ test('nonexisting', target_is_directory=False)
|
||||||
|
+ with self.subTest('nonexisting dir'):
|
||||||
|
+ test('nonexisting', target_is_directory=True)
|
||||||
|
+
|
||||||
|
+ with self.subTest('existing file'):
|
||||||
|
+ os.chmod(file1, mode)
|
||||||
|
+ old_mode = os.stat(file1).st_mode
|
||||||
|
+ test(file1, target_is_directory=False)
|
||||||
|
+ new_mode = os.stat(file1).st_mode
|
||||||
|
+ self.assertEqual(new_mode, old_mode,
|
||||||
|
+ '%03o != %03o' % (new_mode, old_mode))
|
||||||
|
+
|
||||||
|
+ with self.subTest('existing dir'):
|
||||||
|
+ os.chmod(dir1, mode)
|
||||||
|
+ old_mode = os.stat(dir1).st_mode
|
||||||
|
+ test(dir1, target_is_directory=True)
|
||||||
|
+ new_mode = os.stat(dir1).st_mode
|
||||||
|
+ self.assertEqual(new_mode, old_mode,
|
||||||
|
+ '%03o != %03o' % (new_mode, old_mode))
|
||||||
|
+
|
||||||
|
+ @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
|
||||||
|
+ @support.skip_unless_symlink
|
||||||
|
+ def test_cleanup_with_symlink_flags(self):
|
||||||
|
+ # cleanup() should not follow symlinks when fixing flags (#91133)
|
||||||
|
+ flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
|
||||||
|
+ self.check_flags(flags)
|
||||||
|
+
|
||||||
|
+ with self.do_create(recurse=0) as d2:
|
||||||
|
+ file1 = os.path.join(d2, 'file1')
|
||||||
|
+ open(file1, 'wb').close()
|
||||||
|
+ dir1 = os.path.join(d2, 'dir1')
|
||||||
|
+ os.mkdir(dir1)
|
||||||
|
+ def test(target, target_is_directory):
|
||||||
|
+ d1 = self.do_create(recurse=0)
|
||||||
|
+ symlink = os.path.join(d1.name, 'symlink')
|
||||||
|
+ os.symlink(target, symlink,
|
||||||
|
+ target_is_directory=target_is_directory)
|
||||||
|
+ try:
|
||||||
|
+ os.chflags(symlink, flags, follow_symlinks=False)
|
||||||
|
+ except NotImplementedError:
|
||||||
|
+ pass
|
||||||
|
+ try:
|
||||||
|
+ os.chflags(symlink, flags)
|
||||||
|
+ except FileNotFoundError:
|
||||||
|
+ pass
|
||||||
|
+ os.chflags(d1.name, flags)
|
||||||
|
+ d1.cleanup()
|
||||||
|
+ self.assertFalse(os.path.exists(d1.name))
|
||||||
|
+
|
||||||
|
+ with self.subTest('nonexisting file'):
|
||||||
|
+ test('nonexisting', target_is_directory=False)
|
||||||
|
+ with self.subTest('nonexisting dir'):
|
||||||
|
+ test('nonexisting', target_is_directory=True)
|
||||||
|
+
|
||||||
|
+ with self.subTest('existing file'):
|
||||||
|
+ os.chflags(file1, flags)
|
||||||
|
+ old_flags = os.stat(file1).st_flags
|
||||||
|
+ test(file1, target_is_directory=False)
|
||||||
|
+ new_flags = os.stat(file1).st_flags
|
||||||
|
+ self.assertEqual(new_flags, old_flags)
|
||||||
|
+
|
||||||
|
+ with self.subTest('existing dir'):
|
||||||
|
+ os.chflags(dir1, flags)
|
||||||
|
+ old_flags = os.stat(dir1).st_flags
|
||||||
|
+ test(dir1, target_is_directory=True)
|
||||||
|
+ new_flags = os.stat(dir1).st_flags
|
||||||
|
+ self.assertEqual(new_flags, old_flags)
|
||||||
|
+
|
||||||
|
@support.cpython_only
|
||||||
|
def test_del_on_collection(self):
|
||||||
|
# A TemporaryDirectory is deleted when garbage collected
|
||||||
|
@@ -1506,9 +1603,27 @@ def test_modes(self):
|
||||||
|
d.cleanup()
|
||||||
|
self.assertFalse(os.path.exists(d.name))
|
||||||
|
|
||||||
|
- @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.lchflags')
|
||||||
|
+ def check_flags(self, flags):
|
||||||
|
+ # skip the test if these flags are not supported (ex: FreeBSD 13)
|
||||||
|
+ filename = support.TESTFN
|
||||||
|
+ try:
|
||||||
|
+ open(filename, "w").close()
|
||||||
|
+ try:
|
||||||
|
+ os.chflags(filename, flags)
|
||||||
|
+ except OSError as exc:
|
||||||
|
+ # "OSError: [Errno 45] Operation not supported"
|
||||||
|
+ self.skipTest(f"chflags() doesn't support flags "
|
||||||
|
+ f"{flags:#b}: {exc}")
|
||||||
|
+ else:
|
||||||
|
+ os.chflags(filename, 0)
|
||||||
|
+ finally:
|
||||||
|
+ support.unlink(filename)
|
||||||
|
+
|
||||||
|
+ @unittest.skipUnless(hasattr(os, 'chflags'), 'requires os.chflags')
|
||||||
|
def test_flags(self):
|
||||||
|
flags = stat.UF_IMMUTABLE | stat.UF_NOUNLINK
|
||||||
|
+ self.check_flags(flags)
|
||||||
|
+
|
||||||
|
d = self.do_create(recurse=3, dirs=2, files=2)
|
||||||
|
with d:
|
||||||
|
# Change files and directories flags recursively.
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst b/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000000..7991048fc48e03
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Library/2022-12-01-16-57-44.gh-issue-91133.LKMVCV.rst
|
||||||
|
@@ -0,0 +1,2 @@
|
||||||
|
+Fix a bug in :class:`tempfile.TemporaryDirectory` cleanup, which now no longer
|
||||||
|
+dereferences symlinks when working around file system permission errors.
|
@ -0,0 +1,143 @@
|
|||||||
|
From a2c59992e9e8d35baba9695eb186ad6c6ff85c51 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Wed, 17 Jan 2024 14:48:06 +0100
|
||||||
|
Subject: [PATCH] [3.9] gh-109858: Protect zipfile from "quoted-overlap"
|
||||||
|
zipbomb (GH-110016) (GH-113915)
|
||||||
|
|
||||||
|
Raise BadZipFile when try to read an entry that overlaps with other entry or
|
||||||
|
central directory.
|
||||||
|
(cherry picked from commit 66363b9a7b9fe7c99eba3a185b74c5fdbf842eba)
|
||||||
|
|
||||||
|
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
---
|
||||||
|
Lib/test/test_zipfile.py | 60 +++++++++++++++++++
|
||||||
|
Lib/zipfile.py | 12 ++++
|
||||||
|
...-09-28-13-15-51.gh-issue-109858.43e2dg.rst | 3 +
|
||||||
|
3 files changed, 75 insertions(+)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py
|
||||||
|
index bd383d3f68552b..17e95eb86239a5 100644
|
||||||
|
--- a/Lib/test/test_zipfile.py
|
||||||
|
+++ b/Lib/test/test_zipfile.py
|
||||||
|
@@ -2045,6 +2045,66 @@ def test_decompress_without_3rd_party_library(self):
|
||||||
|
with zipfile.ZipFile(zip_file) as zf:
|
||||||
|
self.assertRaises(RuntimeError, zf.extract, 'a.txt')
|
||||||
|
|
||||||
|
+ @requires_zlib()
|
||||||
|
+ def test_full_overlap(self):
|
||||||
|
+ data = (
|
||||||
|
+ b'PK\x03\x04\x14\x00\x00\x00\x08\x00\xa0lH\x05\xe2\x1e'
|
||||||
|
+ b'8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00\x00\x00a\xed'
|
||||||
|
+ b'\xc0\x81\x08\x00\x00\x00\xc00\xd6\xfbK\\d\x0b`P'
|
||||||
|
+ b'K\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0lH\x05\xe2'
|
||||||
|
+ b'\x1e8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00\x00\x00\x00'
|
||||||
|
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00aPK'
|
||||||
|
+ b'\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0lH\x05\xe2\x1e'
|
||||||
|
+ b'8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00\x00\x00\x00\x00'
|
||||||
|
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00bPK\x05'
|
||||||
|
+ b'\x06\x00\x00\x00\x00\x02\x00\x02\x00^\x00\x00\x00/\x00\x00'
|
||||||
|
+ b'\x00\x00\x00'
|
||||||
|
+ )
|
||||||
|
+ with zipfile.ZipFile(io.BytesIO(data), 'r') as zipf:
|
||||||
|
+ self.assertEqual(zipf.namelist(), ['a', 'b'])
|
||||||
|
+ zi = zipf.getinfo('a')
|
||||||
|
+ self.assertEqual(zi.header_offset, 0)
|
||||||
|
+ self.assertEqual(zi.compress_size, 16)
|
||||||
|
+ self.assertEqual(zi.file_size, 1033)
|
||||||
|
+ zi = zipf.getinfo('b')
|
||||||
|
+ self.assertEqual(zi.header_offset, 0)
|
||||||
|
+ self.assertEqual(zi.compress_size, 16)
|
||||||
|
+ self.assertEqual(zi.file_size, 1033)
|
||||||
|
+ self.assertEqual(len(zipf.read('a')), 1033)
|
||||||
|
+ with self.assertRaisesRegex(zipfile.BadZipFile, 'File name.*differ'):
|
||||||
|
+ zipf.read('b')
|
||||||
|
+
|
||||||
|
+ @requires_zlib()
|
||||||
|
+ def test_quoted_overlap(self):
|
||||||
|
+ data = (
|
||||||
|
+ b'PK\x03\x04\x14\x00\x00\x00\x08\x00\xa0lH\x05Y\xfc'
|
||||||
|
+ b'8\x044\x00\x00\x00(\x04\x00\x00\x01\x00\x00\x00a\x00'
|
||||||
|
+ b'\x1f\x00\xe0\xffPK\x03\x04\x14\x00\x00\x00\x08\x00\xa0l'
|
||||||
|
+ b'H\x05\xe2\x1e8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00'
|
||||||
|
+ b'\x00\x00b\xed\xc0\x81\x08\x00\x00\x00\xc00\xd6\xfbK\\'
|
||||||
|
+ b'd\x0b`PK\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0'
|
||||||
|
+ b'lH\x05Y\xfc8\x044\x00\x00\x00(\x04\x00\x00\x01'
|
||||||
|
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
+ b'\x00aPK\x01\x02\x14\x00\x14\x00\x00\x00\x08\x00\xa0l'
|
||||||
|
+ b'H\x05\xe2\x1e8\xbb\x10\x00\x00\x00\t\x04\x00\x00\x01\x00'
|
||||||
|
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x00\x00'
|
||||||
|
+ b'bPK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00^\x00\x00'
|
||||||
|
+ b'\x00S\x00\x00\x00\x00\x00'
|
||||||
|
+ )
|
||||||
|
+ with zipfile.ZipFile(io.BytesIO(data), 'r') as zipf:
|
||||||
|
+ self.assertEqual(zipf.namelist(), ['a', 'b'])
|
||||||
|
+ zi = zipf.getinfo('a')
|
||||||
|
+ self.assertEqual(zi.header_offset, 0)
|
||||||
|
+ self.assertEqual(zi.compress_size, 52)
|
||||||
|
+ self.assertEqual(zi.file_size, 1064)
|
||||||
|
+ zi = zipf.getinfo('b')
|
||||||
|
+ self.assertEqual(zi.header_offset, 36)
|
||||||
|
+ self.assertEqual(zi.compress_size, 16)
|
||||||
|
+ self.assertEqual(zi.file_size, 1033)
|
||||||
|
+ with self.assertRaisesRegex(zipfile.BadZipFile, 'Overlapped entries'):
|
||||||
|
+ zipf.read('a')
|
||||||
|
+ self.assertEqual(len(zipf.read('b')), 1033)
|
||||||
|
+
|
||||||
|
def tearDown(self):
|
||||||
|
unlink(TESTFN)
|
||||||
|
unlink(TESTFN2)
|
||||||
|
diff --git a/Lib/zipfile.py b/Lib/zipfile.py
|
||||||
|
index 1e942a503e8ee1..95f95ee112667a 100644
|
||||||
|
--- a/Lib/zipfile.py
|
||||||
|
+++ b/Lib/zipfile.py
|
||||||
|
@@ -338,6 +338,7 @@ class ZipInfo (object):
|
||||||
|
'compress_size',
|
||||||
|
'file_size',
|
||||||
|
'_raw_time',
|
||||||
|
+ '_end_offset',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
|
||||||
|
@@ -379,6 +380,7 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
|
||||||
|
self.external_attr = 0 # External file attributes
|
||||||
|
self.compress_size = 0 # Size of the compressed file
|
||||||
|
self.file_size = 0 # Size of the uncompressed file
|
||||||
|
+ self._end_offset = None # Start of the next local header or central directory
|
||||||
|
# Other attributes are set by class ZipFile:
|
||||||
|
# header_offset Byte offset to the file header
|
||||||
|
# CRC CRC-32 of the uncompressed file
|
||||||
|
@@ -1399,6 +1401,12 @@ def _RealGetContents(self):
|
||||||
|
if self.debug > 2:
|
||||||
|
print("total", total)
|
||||||
|
|
||||||
|
+ end_offset = self.start_dir
|
||||||
|
+ for zinfo in sorted(self.filelist,
|
||||||
|
+ key=lambda zinfo: zinfo.header_offset,
|
||||||
|
+ reverse=True):
|
||||||
|
+ zinfo._end_offset = end_offset
|
||||||
|
+ end_offset = zinfo.header_offset
|
||||||
|
|
||||||
|
def namelist(self):
|
||||||
|
"""Return a list of file names in the archive."""
|
||||||
|
@@ -1554,6 +1562,10 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False):
|
||||||
|
'File name in directory %r and header %r differ.'
|
||||||
|
% (zinfo.orig_filename, fname))
|
||||||
|
|
||||||
|
+ if (zinfo._end_offset is not None and
|
||||||
|
+ zef_file.tell() + zinfo.compress_size > zinfo._end_offset):
|
||||||
|
+ raise BadZipFile(f"Overlapped entries: {zinfo.orig_filename!r} (possible zip bomb)")
|
||||||
|
+
|
||||||
|
# check for encrypted flag & handle password
|
||||||
|
is_encrypted = zinfo.flag_bits & 0x1
|
||||||
|
if is_encrypted:
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst b/Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 00000000000000..be279caffc46ee
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+Protect :mod:`zipfile` from "quoted-overlap" zipbomb. It now raises
|
||||||
|
+BadZipFile when try to read an entry that overlaps with other entry or
|
||||||
|
+central directory.
|
@ -0,0 +1,245 @@
|
|||||||
|
From b4225ca91547aa97ed3aca391614afbb255bc877 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Seth Michael Larson <seth@python.org>
|
||||||
|
Date: Wed, 4 Sep 2024 10:46:01 -0500
|
||||||
|
Subject: [PATCH] [3.9] gh-121285: Remove backtracking when parsing tarfile
|
||||||
|
headers (GH-121286) (#123641)
|
||||||
|
|
||||||
|
* 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 | 105 +++++++++++-------
|
||||||
|
Lib/test/test_tarfile.py | 42 +++++++
|
||||||
|
...-07-02-13-39-20.gh-issue-121285.hrl-yI.rst | 2 +
|
||||||
|
3 files changed, 111 insertions(+), 38 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 7a6158c2eb9893..d75ba50b6670c7 100755
|
||||||
|
--- a/Lib/tarfile.py
|
||||||
|
+++ b/Lib/tarfile.py
|
||||||
|
@@ -840,6 +840,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.
|
||||||
|
@@ -1399,41 +1402,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 True:
|
||||||
|
- match = regex.match(buf, pos)
|
||||||
|
- if not match:
|
||||||
|
- break
|
||||||
|
+ 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")
|
||||||
|
|
||||||
|
- length, keyword = match.groups()
|
||||||
|
- length = int(length)
|
||||||
|
- if length == 0:
|
||||||
|
+ 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
|
||||||
|
@@ -1441,17 +1462,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:
|
||||||
|
@@ -1466,7 +1486,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.
|
||||||
|
@@ -1488,15 +1508,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 3df64c78032275..2218401e3867be 100644
|
||||||
|
--- a/Lib/test/test_tarfile.py
|
||||||
|
+++ b/Lib/test/test_tarfile.py
|
||||||
|
@@ -1113,6 +1113,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"file could not be opened successfully"):
|
||||||
|
+ 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,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCgAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmTnntEACgkQsmmV4xAl
|
||||||
|
BWgmQw/9EFWMXtSfWBV93AQF37r0nbUnOBvrOcubkO7ygt+GfHKzN8EPuNeO2It7
|
||||||
|
yNZDuCmwepnNGaIkO7UkgbwYyNw3YaoHQqxG8izAfJAVqK6BSk8UAET/YKWFXbLv
|
||||||
|
cZBfgxSa0tTEkwq3BAY4vDewRXnLkUq7k6JRRCKFGLNSi/ygC56SijxyAV2g4Vio
|
||||||
|
Qcwr9VhsTvz6ujoWuPrfVpUY4I81LBJxKK7n9zBreYzh5uUXRu5k4lN2W8HrE4q0
|
||||||
|
7tTdsccB9j1CJAiUacYLxTFsvwd/hBs9+g9Eu5kqGeChqEU56Gd8wR96TEu8cVIZ
|
||||||
|
Bv5UEo9MgT1KsJwk0FMfV8qVScqZrGG3QaoMtNAeAm/tUrhhZO9ANYsC9dey03ut
|
||||||
|
tU6s5GAeh6i17bqW5WfvzCdhY9ayCInndzkq7SPi9F7fYx79PgdsofqPdyCSBXUo
|
||||||
|
Ozfn1VQkYQJTmYtrwqLfdAivubaEPIf1+fLqMOXbrI85Ujuy5xzlgVrrqO2K9rbE
|
||||||
|
DYyPgGZjPtss/yZGRCUdJX6rbW8Tq0HKt/8HpbW5fCt9o0wCSawR71GhzPA1fpNs
|
||||||
|
0mkAGvvoNGdiSizTLLPvNCaecw4kSzeBNViyP6oRCv69ifNqHPErItsMZ0YIMU14
|
||||||
|
w4/d9yI9kUa2bvE3cmx6G+9OS8PYip9MsJbQgP7kJsZ8wgt9rQU=
|
||||||
|
=aw+P
|
||||||
|
-----END PGP SIGNATURE-----
|
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQIzBAABCgAdFiEE4/8oOcBIslwITevpsmmV4xAlBWgFAmX5uMIACgkQsmmV4xAl
|
|
||||||
BWj1tQ//T2qX0m08xWGV7az0D1sH3qjoY+4fEYrknw5uAHqZFiQecRsF27jxv6iH
|
|
||||||
gP/6GAUw+lbH+9UofhCc0NbPOklliS7gFLNqJdKYFB6JXRNxiRYKh3uVx5o2n0ES
|
|
||||||
kR3kRl77S47rtCbSMrKTh6ZoWowyIUZGFsIonk5KsLv+oELXY1AK/Im9i3/iTJ1Z
|
|
||||||
jd/e2oHWuseIxbGZAO8AEP8zOsMMIHfsL3ry8H9xhhPyQM6t5DldqLH3UVE6kq95
|
|
||||||
fs+olGO4FEKif3VDuLaHVlgtGZOUr6aDIYUmWxctPicboSb6RJAq37CCYgWykOyB
|
|
||||||
WQec0ONbU7lxt5jhemLSDRy0mEio7+nXIKsO9rDN0Wk1QMpHUl77/C5qVlzfHal7
|
|
||||||
NhPt8Yl0hBnOjzTq+di+xhAKJcdKp+zZH7/ugAbthuqhNfnkqiF68PANHrCm3gbY
|
|
||||||
myN0eSaQ9yIa/MbHW8Am9NL/nuFbxdJUL/OIKQ9kFHgD7Qid86TZF0G2vbiBH/eF
|
|
||||||
IVYoMxRZLd7eu5dIcwXSef+Ai97pODbx9y7bOCFyBO9FuFrlhPObgc7KXCeAzP+y
|
|
||||||
k5eWvZtWTvvQ+2si2iT22EPBO0D0pnhYWZKpGK5EuKuw8nasNS1yLbhDTVpARynd
|
|
||||||
8buQh3t2wPfILlQr0+JzDY8GSdQ/nIHGgx2IERdSX/v+9Yo2AvU=
|
|
||||||
=gYAl
|
|
||||||
-----END PGP SIGNATURE-----
|
|
Loading…
Reference in new issue