Sync from SUSE:SLFO:Main python311 revision e9d1dfb702590560732243725ddec50f
This commit is contained in:
parent
5cf4c2d684
commit
770b9661f8
155
CVE-2023-52425-libexpat-2.6.0-backport.patch
Normal file
155
CVE-2023-52425-libexpat-2.6.0-backport.patch
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
From 7da97f61816f3cadaa6788804b22a2434b40e8c5 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Mon, 21 Feb 2022 08:16:09 -0800
|
||||||
|
Subject: [PATCH] bpo-46811: Make test suite support Expat >=2.4.5 (GH-31453)
|
||||||
|
(GH-31472)
|
||||||
|
|
||||||
|
Curly brackets were never allowed in namespace URIs
|
||||||
|
according to RFC 3986, and so-called namespace-validating
|
||||||
|
XML parsers have the right to reject them a invalid URIs.
|
||||||
|
|
||||||
|
libexpat >=2.4.5 has become strcter in that regard due to
|
||||||
|
related security issues; with ET.XML instantiating a
|
||||||
|
namespace-aware parser under the hood, this test has no
|
||||||
|
future in CPython.
|
||||||
|
|
||||||
|
References:
|
||||||
|
- https://datatracker.ietf.org/doc/html/rfc3968
|
||||||
|
- https://www.w3.org/TR/xml-names/
|
||||||
|
|
||||||
|
Also, test_minidom.py: Support Expat >=2.4.5
|
||||||
|
(cherry picked from commit 2cae93832f46b245847bdc252456ddf7742ef45e)
|
||||||
|
|
||||||
|
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
|
||||||
|
---
|
||||||
|
Lib/test/test_minidom.py | 23 +---
|
||||||
|
Lib/test/test_xml_etree.py | 50 +++++-----
|
||||||
|
Misc/NEWS.d/next/Library/2024-02-08-14-21-28.gh-issue-115133.ycl4ko.rst | 2
|
||||||
|
3 files changed, 39 insertions(+), 36 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst
|
||||||
|
|
||||||
|
--- a/Lib/test/test_minidom.py
|
||||||
|
+++ b/Lib/test/test_minidom.py
|
||||||
|
@@ -6,7 +6,6 @@ import io
|
||||||
|
from test import support
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
-import pyexpat
|
||||||
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
from xml.dom.minidom import parse, Attr, Node, Document, parseString
|
||||||
|
@@ -1163,13 +1162,11 @@ class MinidomTest(unittest.TestCase):
|
||||||
|
|
||||||
|
# Verify that character decoding errors raise exceptions instead
|
||||||
|
# of crashing
|
||||||
|
- if pyexpat.version_info >= (2, 4, 5):
|
||||||
|
- self.assertRaises(ExpatError, parseString,
|
||||||
|
- b'<fran\xe7ais></fran\xe7ais>')
|
||||||
|
- self.assertRaises(ExpatError, parseString,
|
||||||
|
- b'<franais>Comment \xe7a va ? Tr\xe8s bien ?</franais>')
|
||||||
|
- else:
|
||||||
|
- self.assertRaises(UnicodeDecodeError, parseString,
|
||||||
|
+ # It doesn’t make any sense to insist on the exact text of the
|
||||||
|
+ # error message, or even the exact Exception … it is enough that
|
||||||
|
+ # the error has been discovered.
|
||||||
|
+ with self.assertRaises((UnicodeDecodeError, ExpatError)):
|
||||||
|
+ parseString(
|
||||||
|
b'<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
|
||||||
|
|
||||||
|
doc.unlink()
|
||||||
|
@@ -1631,12 +1628,10 @@ class MinidomTest(unittest.TestCase):
|
||||||
|
self.confirm(doc2.namespaceURI == xml.dom.EMPTY_NAMESPACE)
|
||||||
|
|
||||||
|
def testExceptionOnSpacesInXMLNSValue(self):
|
||||||
|
- if pyexpat.version_info >= (2, 4, 5):
|
||||||
|
- context = self.assertRaisesRegex(ExpatError, 'syntax error')
|
||||||
|
- else:
|
||||||
|
- context = self.assertRaisesRegex(ValueError, 'Unsupported syntax')
|
||||||
|
-
|
||||||
|
- with context:
|
||||||
|
+ # It doesn’t make any sense to insist on the exact text of the
|
||||||
|
+ # error message, or even the exact Exception … it is enough that
|
||||||
|
+ # the error has been discovered.
|
||||||
|
+ with self.assertRaises((ExpatError, ValueError)):
|
||||||
|
parseString('<element xmlns:abc="http:abc.com/de f g/hi/j k"><abc:foo /></element>')
|
||||||
|
|
||||||
|
def testDocRemoveChild(self):
|
||||||
|
--- a/Lib/test/test_xml_etree.py
|
||||||
|
+++ b/Lib/test/test_xml_etree.py
|
||||||
|
@@ -13,6 +13,7 @@ import itertools
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
+import pyexpat
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import types
|
||||||
|
@@ -120,6 +121,10 @@ ATTLIST_XML = """\
|
||||||
|
</foo>
|
||||||
|
"""
|
||||||
|
|
||||||
|
+fails_with_expat_2_6_0 = (unittest.expectedFailure
|
||||||
|
+ if pyexpat.version_info >= (2, 6, 0) else
|
||||||
|
+ lambda test: test)
|
||||||
|
+
|
||||||
|
def checkwarnings(*filters, quiet=False):
|
||||||
|
def decorator(test):
|
||||||
|
def newtest(*args, **kwargs):
|
||||||
|
@@ -1400,28 +1405,29 @@ class XMLPullParserTest(unittest.TestCas
|
||||||
|
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())
|
||||||
|
+
|
||||||
|
+ def test_simple_xml_chunk_22(self):
|
||||||
|
+ self.test_simple_xml(chunk_size=22)
|
||||||
|
|
||||||
|
def test_feed_while_iterating(self):
|
||||||
|
parser = ET.XMLPullParser()
|
||||||
|
--- /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.
|
145
CVE-2024-0397-memrace_ssl.SSLContext_cert_store.patch
Normal file
145
CVE-2024-0397-memrace_ssl.SSLContext_cert_store.patch
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
From fa5c6d1c4b3e6556cddf663d7b36ed7cdbbde18c Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Benjamin <davidben@google.com>
|
||||||
|
Date: Thu, 15 Feb 2024 19:24:51 -0500
|
||||||
|
Subject: [PATCH] gh-114572: Fix locking in cert_store_stats and get_ca_certs
|
||||||
|
(GH-114573)
|
||||||
|
|
||||||
|
* gh-114572: Fix locking in cert_store_stats and get_ca_certs
|
||||||
|
|
||||||
|
cert_store_stats and get_ca_certs query the SSLContext's X509_STORE with
|
||||||
|
X509_STORE_get0_objects, but reading the result requires a lock. See
|
||||||
|
https://github.com/openssl/openssl/pull/23224 for details.
|
||||||
|
|
||||||
|
Instead, use X509_STORE_get1_objects, newly added in that PR.
|
||||||
|
X509_STORE_get1_objects does not exist in current OpenSSLs, but we can
|
||||||
|
polyfill it with X509_STORE_lock and X509_STORE_unlock.
|
||||||
|
|
||||||
|
* Work around const-correctness problem
|
||||||
|
|
||||||
|
* Add missing X509_STORE_get1_objects failure check
|
||||||
|
|
||||||
|
* Add blurb
|
||||||
|
(cherry picked from commit bce693111bff906ccf9281c22371331aaff766ab)
|
||||||
|
|
||||||
|
Co-authored-by: David Benjamin <davidben@google.com>
|
||||||
|
---
|
||||||
|
Misc/NEWS.d/next/Security/2024-01-26-22-14-09.gh-issue-114572.t1QMQD.rst | 4
|
||||||
|
Modules/_ssl.c | 65 +++++++++-
|
||||||
|
2 files changed, 64 insertions(+), 5 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Security/2024-01-26-22-14-09.gh-issue-114572.t1QMQD.rst
|
||||||
|
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Security/2024-01-26-22-14-09.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.
|
||||||
|
--- a/Modules/_ssl.c
|
||||||
|
+++ b/Modules/_ssl.c
|
||||||
|
@@ -4529,6 +4529,50 @@ set_sni_callback(PySSLContext *self, PyO
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER < 0x30300000L
|
||||||
|
+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\
|
||||||
|
@@ -4558,7 +4602,12 @@ _ssl__SSLContext_cert_store_stats_impl(P
|
||||||
|
int x509 = 0, crl = 0, ca = 0, i;
|
||||||
|
|
||||||
|
store = SSL_CTX_get_cert_store(self->ctx);
|
||||||
|
- objs = X509_STORE_get0_objects(store);
|
||||||
|
+ objs = X509_STORE_get1_objects(store);
|
||||||
|
+ if (objs == NULL) {
|
||||||
|
+ PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
|
||||||
|
obj = sk_X509_OBJECT_value(objs, i);
|
||||||
|
switch (X509_OBJECT_get_type(obj)) {
|
||||||
|
@@ -4572,12 +4621,11 @@ _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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
|
||||||
|
return Py_BuildValue("{sisisi}", "x509", x509, "crl", crl,
|
||||||
|
"x509_ca", ca);
|
||||||
|
}
|
||||||
|
@@ -4609,7 +4657,12 @@ _ssl__SSLContext_get_ca_certs_impl(PySSL
|
||||||
|
}
|
||||||
|
|
||||||
|
store = SSL_CTX_get_cert_store(self->ctx);
|
||||||
|
- objs = X509_STORE_get0_objects(store);
|
||||||
|
+ objs = X509_STORE_get1_objects(store);
|
||||||
|
+ if (objs == NULL) {
|
||||||
|
+ PyErr_SetString(PyExc_MemoryError, "failed to query cert store");
|
||||||
|
+ goto error;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
for (i = 0; i < sk_X509_OBJECT_num(objs); i++) {
|
||||||
|
X509_OBJECT *obj;
|
||||||
|
X509 *cert;
|
||||||
|
@@ -4637,9 +4690,11 @@ _ssl__SSLContext_get_ca_certs_impl(PySSL
|
||||||
|
}
|
||||||
|
Py_CLEAR(ci);
|
||||||
|
}
|
||||||
|
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
|
||||||
|
return rlist;
|
||||||
|
|
||||||
|
error:
|
||||||
|
+ sk_X509_OBJECT_pop_free(objs, X509_OBJECT_free);
|
||||||
|
Py_XDECREF(ci);
|
||||||
|
Py_XDECREF(rlist);
|
||||||
|
return NULL;
|
119
CVE-2024-0450-zipfile-avoid-quoted-overlap-zipbomb.patch
Normal file
119
CVE-2024-0450-zipfile-avoid-quoted-overlap-zipbomb.patch
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
From 8281fc11b47064f9a4908358befa9db6829f8b88 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
Date: Wed, 10 Jan 2024 15:55:36 +0200
|
||||||
|
Subject: [PATCH] gh-109858: Protect zipfile from "quoted-overlap" zipbomb
|
||||||
|
(GH-110016)
|
||||||
|
|
||||||
|
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 | 10 +
|
||||||
|
Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst | 3
|
||||||
|
3 files changed, 73 insertions(+)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2023-09-28-13-15-51.gh-issue-109858.43e2dg.rst
|
||||||
|
|
||||||
|
--- a/Lib/test/test_zipfile.py
|
||||||
|
+++ b/Lib/test/test_zipfile.py
|
||||||
|
@@ -2304,6 +2304,66 @@ class OtherTests(unittest.TestCase):
|
||||||
|
zipf.read('a')
|
||||||
|
self.assertEqual(len(zipf.read('b')), 1033)
|
||||||
|
|
||||||
|
+ @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)
|
||||||
|
--- a/Lib/zipfile.py
|
||||||
|
+++ b/Lib/zipfile.py
|
||||||
|
@@ -1217,6 +1217,12 @@ class _ZipWriteFile(io.BufferedIOBase):
|
||||||
|
self._zipfile._writing = False
|
||||||
|
|
||||||
|
|
||||||
|
+ end_offset = self._zipfile.start_dir
|
||||||
|
+ for zinfo in sorted(self._zipfile.filelist,
|
||||||
|
+ key=lambda zinfo: zinfo.header_offset,
|
||||||
|
+ reverse=True):
|
||||||
|
+ zinfo._end_offset = end_offset
|
||||||
|
+ end_offset = zinfo.header_offset
|
||||||
|
|
||||||
|
class ZipFile:
|
||||||
|
""" Class with methods to open, read, write, close, list zip files.
|
||||||
|
@@ -1600,6 +1606,10 @@ class ZipFile:
|
||||||
|
|
||||||
|
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)")
|
||||||
|
+
|
||||||
|
+ 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
|
||||||
|
--- /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.
|
366
CVE-2024-4032-private-IP-addrs.patch
Normal file
366
CVE-2024-4032-private-IP-addrs.patch
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
From b47c766d6085d7918edd7715750d135868fdafd6 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <encukou@gmail.com>
|
||||||
|
Date: Wed, 24 Apr 2024 14:29:30 +0200
|
||||||
|
Subject: [PATCH] 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.11.rst | 9
|
||||||
|
Lib/ipaddress.py | 105 +++++++---
|
||||||
|
Lib/test/test_ipaddress.py | 21 +-
|
||||||
|
Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 9
|
||||||
|
5 files changed, 160 insertions(+), 27 deletions(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
|
||||||
|
|
||||||
|
--- a/Doc/library/ipaddress.rst
|
||||||
|
+++ b/Doc/library/ipaddress.rst
|
||||||
|
@@ -188,18 +188,53 @@ write code that handles both IP versions
|
||||||
|
|
||||||
|
.. 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.11.10
|
||||||
|
+
|
||||||
|
+ 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.11.10
|
||||||
|
+
|
||||||
|
+ 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)
|
||||||
|
--- a/Doc/whatsnew/3.11.rst
|
||||||
|
+++ b/Doc/whatsnew/3.11.rst
|
||||||
|
@@ -2727,3 +2727,12 @@ OpenSSL
|
||||||
|
* Windows builds and macOS installers from python.org now use OpenSSL 3.0.
|
||||||
|
|
||||||
|
.. _libb2: https://www.blake2.net/
|
||||||
|
+
|
||||||
|
+Notable changes in 3.11.10
|
||||||
|
+==========================
|
||||||
|
+
|
||||||
|
+ipaddress
|
||||||
|
+---------
|
||||||
|
+
|
||||||
|
+* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
|
||||||
|
+ ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
|
||||||
|
--- a/Lib/ipaddress.py
|
||||||
|
+++ b/Lib/ipaddress.py
|
||||||
|
@@ -1086,7 +1086,11 @@ class _BaseNetwork(_IPAddressBase):
|
||||||
|
"""
|
||||||
|
return any(self.network_address in priv_network and
|
||||||
|
self.broadcast_address in priv_network
|
||||||
|
- for priv_network in self._constants._private_networks)
|
||||||
|
+ for priv_network in self._constants._private_networks) and all(
|
||||||
|
+ self.network_address not in network and
|
||||||
|
+ self.broadcast_address not in network
|
||||||
|
+ for network in self._constants._private_networks_exceptions
|
||||||
|
+ )
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_global(self):
|
||||||
|
@@ -1333,18 +1337,41 @@ class IPv4Address(_BaseV4, _BaseAddress)
|
||||||
|
@property
|
||||||
|
@functools.lru_cache()
|
||||||
|
def is_private(self):
|
||||||
|
- """Test if this address is allocated for private networks.
|
||||||
|
-
|
||||||
|
- Returns:
|
||||||
|
- A boolean, True if the address is reserved per
|
||||||
|
- iana-ipv4-special-registry.
|
||||||
|
-
|
||||||
|
- """
|
||||||
|
- return any(self in net for net in self._constants._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:
|
||||||
|
+
|
||||||
|
+ * ``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)
|
||||||
|
+ 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
|
||||||
|
@@ -1548,13 +1575,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'),
|
||||||
|
@@ -1565,6 +1594,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')
|
||||||
|
@@ -2010,27 +2044,42 @@ class IPv6Address(_BaseV6, _BaseAddress)
|
||||||
|
@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:
|
||||||
|
+
|
||||||
|
+ * ``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`)::
|
||||||
|
|
||||||
|
- Returns:
|
||||||
|
- A boolean, True if the address is reserved per
|
||||||
|
- iana-ipv6-special-registry, or is ipv4_mapped and is
|
||||||
|
- reserved in the iana-ipv4-special-registry.
|
||||||
|
+ 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``.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
+ 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:
|
||||||
|
+
|
||||||
|
+ 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`)::
|
||||||
|
|
||||||
|
- Returns:
|
||||||
|
- A boolean, true if the address is not reserved per
|
||||||
|
- iana-ipv6-special-registry.
|
||||||
|
+ 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
|
||||||
|
|
||||||
|
@@ -2271,19 +2320,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'),
|
||||||
|
--- a/Lib/test/test_ipaddress.py
|
||||||
|
+++ b/Lib/test/test_ipaddress.py
|
||||||
|
@@ -2269,6 +2269,10 @@ class IpaddrUnitTest(unittest.TestCase):
|
||||||
|
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)
|
||||||
|
@@ -2294,6 +2298,7 @@ class IpaddrUnitTest(unittest.TestCase):
|
||||||
|
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)
|
||||||
|
@@ -2310,8 +2315,8 @@ class IpaddrUnitTest(unittest.TestCase):
|
||||||
|
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::/23").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)
|
||||||
|
@@ -2390,6 +2395,20 @@ class IpaddrUnitTest(unittest.TestCase):
|
||||||
|
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)
|
||||||
|
--- /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.
|
348
CVE-2024-6923-email-hdr-inject.patch
Normal file
348
CVE-2024-6923-email-hdr-inject.patch
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
From f9ddc53ea850fb02d640a9b3263756d43fb6d868 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Petr Viktorin <encukou@gmail.com>
|
||||||
|
Date: Wed, 31 Jul 2024 00:19:48 +0200
|
||||||
|
Subject: [PATCH] [3.11] gh-121650: Encode newlines in headers, and verify
|
||||||
|
headers are sound (GH-122233)
|
||||||
|
|
||||||
|
GH-GH- 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.
|
||||||
|
|
||||||
|
GH-GH- Verify that email headers are well-formed
|
||||||
|
|
||||||
|
This should fail for custom fold() implementations that aren't careful
|
||||||
|
about newlines.
|
||||||
|
|
||||||
|
(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
|
||||||
|
|
||||||
|
Co-authored-by: Petr Viktorin <encukou@gmail.com>
|
||||||
|
Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
|
||||||
|
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
|
||||||
|
---
|
||||||
|
Doc/library/email.errors.rst | 7 +
|
||||||
|
Doc/library/email.policy.rst | 18 ++
|
||||||
|
Doc/whatsnew/3.11.rst | 13 ++
|
||||||
|
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 ++++
|
||||||
|
Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst | 5
|
||||||
|
10 files changed, 164 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
|
||||||
|
@@ -58,6 +58,13 @@ The following exception classes are defi
|
||||||
|
: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.
|
||||||
|
--- a/Doc/library/email.policy.rst
|
||||||
|
+++ b/Doc/library/email.policy.rst
|
||||||
|
@@ -228,6 +228,24 @@ added matters. To illustrate::
|
||||||
|
|
||||||
|
.. versionadded:: 3.6
|
||||||
|
|
||||||
|
+
|
||||||
|
+ .. attribute:: verify_generated_headers
|
||||||
|
+
|
||||||
|
+ If ``True`` (the default), the generator will raise
|
||||||
|
+ :exc:`~email.errors.HeaderWriteError` instead of writing a header
|
||||||
|
+ that is improperly folded or delimited, such that it would
|
||||||
|
+ be parsed as multiple headers or joined with adjacent data.
|
||||||
|
+ Such headers can be generated by custom header classes or bugs
|
||||||
|
+ in the ``email`` module.
|
||||||
|
+
|
||||||
|
+ As it's a security feature, this defaults to ``True`` even in the
|
||||||
|
+ :class:`~email.policy.Compat32` policy.
|
||||||
|
+ For backwards compatible, but unsafe, behavior, it must be set to
|
||||||
|
+ ``False`` explicitly.
|
||||||
|
+
|
||||||
|
+ .. versionadded:: 3.11.10
|
||||||
|
+
|
||||||
|
+
|
||||||
|
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.11.rst
|
||||||
|
+++ b/Doc/whatsnew/3.11.rst
|
||||||
|
@@ -2728,6 +2728,7 @@ OpenSSL
|
||||||
|
|
||||||
|
.. _libb2: https://www.blake2.net/
|
||||||
|
|
||||||
|
+
|
||||||
|
Notable changes in 3.11.10
|
||||||
|
==========================
|
||||||
|
|
||||||
|
@@ -2736,3 +2737,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'\"')+'"'
|
||||||
|
@@ -2779,9 +2781,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]')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
--- 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.
|
||||||
|
@@ -294,6 +295,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,108 +0,0 @@
|
|||||||
From f2eebf3c38eae77765247791576b437ec25ccfe2 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Serhiy Storchaka <storchaka@gmail.com>
|
|
||||||
Date: Sun, 11 Feb 2024 12:08:39 +0200
|
|
||||||
Subject: [PATCH] gh-115133: Fix tests for XMLPullParser with Expat 2.6.0
|
|
||||||
(GH-115164)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
|
|
||||||
---
|
|
||||||
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 267982a8233c92..fa03f381fac92a 100644
|
|
||||||
--- a/Lib/test/test_xml_etree.py
|
|
||||||
+++ b/Lib/test/test_xml_etree.py
|
|
||||||
@@ -13,6 +13,7 @@
|
|
||||||
import operator
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
+import pyexpat
|
|
||||||
import sys
|
|
||||||
import textwrap
|
|
||||||
import types
|
|
||||||
@@ -120,6 +121,10 @@
|
|
||||||
</foo>
|
|
||||||
"""
|
|
||||||
|
|
||||||
+fails_with_expat_2_6_0 = (unittest.expectedFailure
|
|
||||||
+ if pyexpat.version_info >= (2, 6, 0) else
|
|
||||||
+ lambda test: test)
|
|
||||||
+
|
|
||||||
def checkwarnings(*filters, quiet=False):
|
|
||||||
def decorator(test):
|
|
||||||
def newtest(*args, **kwargs):
|
|
||||||
@@ -1400,28 +1405,37 @@ def assert_event_tags(self, parser, expected, max_events=None):
|
|
||||||
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())
|
|
||||||
+
|
|
||||||
+ @fails_with_expat_2_6_0
|
|
||||||
+ def test_simple_xml_chunk_1(self):
|
|
||||||
+ self.test_simple_xml(chunk_size=1)
|
|
||||||
+
|
|
||||||
+ @fails_with_expat_2_6_0
|
|
||||||
+ 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 00000000000000..6f1015235cc25d
|
|
||||||
--- /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.
|
|
@ -1,3 +1,34 @@
|
|||||||
|
-------------------------------------------------------------------
|
||||||
|
Sat Aug 3 17:28:26 UTC 2024 - Matej Cepl <mcepl@suse.com>
|
||||||
|
|
||||||
|
- bsc#1221854 (CVE-2024-0450) Add
|
||||||
|
CVE-2024-0450-zipfile-avoid-quoted-overlap-zipbomb.patch
|
||||||
|
detecting the vulnerability of the "quoted-overlap" zipbomb
|
||||||
|
(from gh#python/cpython!110016).
|
||||||
|
- Add CVE-2023-52425-libexpat-2.6.0-backport.patch to fix tests with
|
||||||
|
patched libexpat below 2.6.0 that doesn't update the version number,
|
||||||
|
just in SLE.
|
||||||
|
- Add CVE-2024-4032-private-IP-addrs.patch to fix bsc#1226448
|
||||||
|
(CVE-2024-4032) rearranging definition of private v global IP
|
||||||
|
addresses.
|
||||||
|
- Add CVE-2024-0397-memrace_ssl.SSLContext_cert_store.patch
|
||||||
|
fixing bsc#1226447 (CVE-2024-0397) by removing memory race
|
||||||
|
condition in ssl.SSLContext certificate store methods.
|
||||||
|
- Add CVE-2024-6923-email-hdr-inject.patch to prevent email
|
||||||
|
header injection due to unquoted newlines (bsc#1228780,
|
||||||
|
CVE-2024-6923).
|
||||||
|
- Stop using %%defattr, it seems to be breaking proper executable
|
||||||
|
attributes on /usr/bin/ scripts (bsc#1227378).
|
||||||
|
- Remove included patches:
|
||||||
|
- libexpat260.patch
|
||||||
|
- support-expat-CVE-2022-25236-patched.patch
|
||||||
|
- CVE-2023-52425-remove-reparse_deferral-tests.patch
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Fri Mar 22 21:22:27 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
- Because of bsc#1189495 we have to revert use of %autopatch.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Tue Mar 12 08:44:47 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
Tue Mar 12 08:44:47 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
|
||||||
|
|
||||||
|
@ -155,9 +155,6 @@ Patch10: skip-test_pyobject_freed_is_freed.patch
|
|||||||
# PATCH-FIX-SLE fix_configure_rst.patch bpo#43774 mcepl@suse.com
|
# PATCH-FIX-SLE fix_configure_rst.patch bpo#43774 mcepl@suse.com
|
||||||
# remove duplicate link targets and make documentation with old Sphinx in SLE
|
# remove duplicate link targets and make documentation with old Sphinx in SLE
|
||||||
Patch11: fix_configure_rst.patch
|
Patch11: fix_configure_rst.patch
|
||||||
# PATCH-FIX-UPSTREAM support-expat-CVE-2022-25236-patched.patch jsc#SLE-21253 mcepl@suse.com
|
|
||||||
# Makes Python resilient to changes of API of libexpat
|
|
||||||
Patch12: support-expat-CVE-2022-25236-patched.patch
|
|
||||||
# PATCH-FIX-UPSTREAM skip_if_buildbot-extend.patch gh#python/cpython#103053 mcepl@suse.com
|
# PATCH-FIX-UPSTREAM skip_if_buildbot-extend.patch gh#python/cpython#103053 mcepl@suse.com
|
||||||
# Skip test_freeze_simple_script
|
# Skip test_freeze_simple_script
|
||||||
Patch13: skip_if_buildbot-extend.patch
|
Patch13: skip_if_buildbot-extend.patch
|
||||||
@ -165,15 +162,29 @@ Patch13: skip_if_buildbot-extend.patch
|
|||||||
# Detect email address parsing errors and return empty tuple to
|
# Detect email address parsing errors and return empty tuple to
|
||||||
# indicate the parsing error (old API)
|
# indicate the parsing error (old API)
|
||||||
Patch14: CVE-2023-27043-email-parsing-errors.patch
|
Patch14: CVE-2023-27043-email-parsing-errors.patch
|
||||||
# PATCH-FIX-UPSTREAM libexpat260.patch gh#python/cpython#115289
|
|
||||||
# Fix tests for XMLPullParser with Expat 2.6.0
|
|
||||||
Patch15: libexpat260.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2023-6597-TempDir-cleaning-symlink.patch bsc#1219666 mcepl@suse.com
|
# PATCH-FIX-UPSTREAM CVE-2023-6597-TempDir-cleaning-symlink.patch bsc#1219666 mcepl@suse.com
|
||||||
# tempfile.TemporaryDirectory: fix symlink bug in cleanup (from gh#python/cpython!99930)
|
# tempfile.TemporaryDirectory: fix symlink bug in cleanup (from gh#python/cpython!99930)
|
||||||
Patch16: CVE-2023-6597-TempDir-cleaning-symlink.patch
|
Patch15: CVE-2023-6597-TempDir-cleaning-symlink.patch
|
||||||
# PATCH-FIX-UPSTREAM bsc1221260-test_asyncio-ResourceWarning.patch bsc#1221260 mcepl@suse.com
|
# PATCH-FIX-UPSTREAM bsc1221260-test_asyncio-ResourceWarning.patch bsc#1221260 mcepl@suse.com
|
||||||
# prevent ResourceWarning in test_asyncio tests
|
# prevent ResourceWarning in test_asyncio tests
|
||||||
Patch17: bsc1221260-test_asyncio-ResourceWarning.patch
|
Patch16: bsc1221260-test_asyncio-ResourceWarning.patch
|
||||||
|
# PATCH-FIX-OPENSUSE CVE-2023-52425-libexpat-2.6.0-backport.patch
|
||||||
|
# This problem on libexpat is patched on SLE without version
|
||||||
|
# update, this patch changes the tests to match the libexpat provided
|
||||||
|
# by SUSE
|
||||||
|
Patch17: CVE-2023-52425-libexpat-2.6.0-backport.patch
|
||||||
|
# PATCH-FIX-UPSTREAM CVE-2024-4032-private-IP-addrs.patch bsc#1226448 mcepl@suse.com
|
||||||
|
# rearrange definition of private v global IP addresses
|
||||||
|
Patch18: CVE-2024-4032-private-IP-addrs.patch
|
||||||
|
# PATCH-FIX-UPSTREAM CVE-2024-0450-zipfile-avoid-quoted-overlap-zipbomb.patch bsc#1221854 mcepl@suse.com
|
||||||
|
# detecting the vulnerability of the "quoted-overlap" zipbomb
|
||||||
|
Patch19: CVE-2024-0450-zipfile-avoid-quoted-overlap-zipbomb.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
|
||||||
|
Patch20: CVE-2024-0397-memrace_ssl.SSLContext_cert_store.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
|
||||||
|
Patch21: CVE-2024-6923-email-hdr-inject.patch
|
||||||
BuildRequires: autoconf-archive
|
BuildRequires: autoconf-archive
|
||||||
BuildRequires: automake
|
BuildRequires: automake
|
||||||
BuildRequires: fdupes
|
BuildRequires: fdupes
|
||||||
@ -415,13 +426,30 @@ This package contains libpython3.2 shared library for embedding in
|
|||||||
other applications.
|
other applications.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%autosetup -p1 -N -n %{tarname}
|
%setup -q -n %{tarname}
|
||||||
%autopatch -p1 -M 08
|
%patch -p1 -P 02
|
||||||
|
%patch -p1 -P 03
|
||||||
|
%patch -p1 -P 04
|
||||||
|
%patch -p1 -P 05
|
||||||
|
%patch -p1 -P 06
|
||||||
|
%patch -p1 -P 07
|
||||||
|
%patch -p1 -P 08
|
||||||
|
|
||||||
%if 0%{?suse_version} <= 1500
|
%if 0%{?suse_version} <= 1500
|
||||||
%patch -P 09 -p1
|
%patch -P 09 -p1
|
||||||
%endif
|
%endif
|
||||||
%autopatch -p1 -m 10
|
|
||||||
|
%patch -p1 -P 10
|
||||||
|
%patch -p1 -P 11
|
||||||
|
%patch -p1 -P 13
|
||||||
|
%patch -p1 -P 14
|
||||||
|
%patch -p1 -P 15
|
||||||
|
%patch -p1 -P 16
|
||||||
|
%patch -p1 -P 17
|
||||||
|
%patch -p1 -P 18
|
||||||
|
%patch -p1 -P 19
|
||||||
|
%patch -p1 -P 20
|
||||||
|
%patch -p1 -P 21
|
||||||
|
|
||||||
# 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
|
||||||
@ -776,25 +804,21 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
|
|
||||||
%if %{with general}
|
%if %{with general}
|
||||||
%files -n %{python_pkg_name}-tk
|
%files -n %{python_pkg_name}-tk
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{sitedir}/tkinter
|
%{sitedir}/tkinter
|
||||||
%exclude %{sitedir}/tkinter/test
|
%exclude %{sitedir}/tkinter/test
|
||||||
%{dynlib _tkinter}
|
%{dynlib _tkinter}
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-curses
|
%files -n %{python_pkg_name}-curses
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{sitedir}/curses
|
%{sitedir}/curses
|
||||||
%{dynlib _curses}
|
%{dynlib _curses}
|
||||||
%{dynlib _curses_panel}
|
%{dynlib _curses_panel}
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-dbm
|
%files -n %{python_pkg_name}-dbm
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{sitedir}/dbm
|
%{sitedir}/dbm
|
||||||
%{dynlib _dbm}
|
%{dynlib _dbm}
|
||||||
%{dynlib _gdbm}
|
%{dynlib _gdbm}
|
||||||
|
|
||||||
%files -n %{python_pkg_name}
|
%files -n %{python_pkg_name}
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%dir %{sitedir}
|
%dir %{sitedir}
|
||||||
%dir %{sitedir}/lib-dynload
|
%dir %{sitedir}/lib-dynload
|
||||||
%{sitedir}/sqlite3
|
%{sitedir}/sqlite3
|
||||||
@ -806,7 +830,6 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
%endif
|
%endif
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-idle
|
%files -n %{python_pkg_name}-idle
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{sitedir}/idlelib
|
%{sitedir}/idlelib
|
||||||
%dir %{_sysconfdir}/idle%{python_version}
|
%dir %{_sysconfdir}/idle%{python_version}
|
||||||
%config %{_sysconfdir}/idle%{python_version}/*
|
%config %{_sysconfdir}/idle%{python_version}/*
|
||||||
@ -843,11 +866,9 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
%postun -n libpython%{so_version} -p /sbin/ldconfig
|
%postun -n libpython%{so_version} -p /sbin/ldconfig
|
||||||
|
|
||||||
%files -n libpython%{so_version}
|
%files -n libpython%{so_version}
|
||||||
%defattr(644, root,root)
|
|
||||||
%{_libdir}/libpython%{python_abi}.so.%{so_major}.%{so_minor}
|
%{_libdir}/libpython%{python_abi}.so.%{so_major}.%{so_minor}
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-tools
|
%files -n %{python_pkg_name}-tools
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{sitedir}/turtledemo
|
%{sitedir}/turtledemo
|
||||||
%if %{primary_interpreter}
|
%if %{primary_interpreter}
|
||||||
%{_bindir}/2to3
|
%{_bindir}/2to3
|
||||||
@ -856,7 +877,6 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
%doc %{_docdir}/%{name}/Tools
|
%doc %{_docdir}/%{name}/Tools
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-devel
|
%files -n %{python_pkg_name}-devel
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{_libdir}/libpython%{python_abi}.so
|
%{_libdir}/libpython%{python_abi}.so
|
||||||
%if %{primary_interpreter}
|
%if %{primary_interpreter}
|
||||||
%{_libdir}/libpython3.so
|
%{_libdir}/libpython3.so
|
||||||
@ -864,7 +884,6 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
%{_libdir}/pkgconfig/*
|
%{_libdir}/pkgconfig/*
|
||||||
%{_includedir}/python%{python_abi}
|
%{_includedir}/python%{python_abi}
|
||||||
%{sitedir}/config-%{python_abi}-*
|
%{sitedir}/config-%{python_abi}-*
|
||||||
%defattr(755, root, root)
|
|
||||||
%{_bindir}/python%{python_abi}-config
|
%{_bindir}/python%{python_abi}-config
|
||||||
%if %{primary_interpreter}
|
%if %{primary_interpreter}
|
||||||
%{_bindir}/python3-config
|
%{_bindir}/python3-config
|
||||||
@ -877,7 +896,6 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
%{_datadir}/gdb/auto-load/%{_libdir}/libpython%{python_abi}.so.%{so_major}.%{so_minor}-gdb.py
|
%{_datadir}/gdb/auto-load/%{_libdir}/libpython%{python_abi}.so.%{so_major}.%{so_minor}-gdb.py
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-testsuite
|
%files -n %{python_pkg_name}-testsuite
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
%{sitedir}/test
|
%{sitedir}/test
|
||||||
%{sitedir}/*/test
|
%{sitedir}/*/test
|
||||||
%{sitedir}/*/tests
|
%{sitedir}/*/tests
|
||||||
@ -894,7 +912,6 @@ echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-impo
|
|||||||
%dir %{sitedir}/tkinter
|
%dir %{sitedir}/tkinter
|
||||||
|
|
||||||
%files -n %{python_pkg_name}-base
|
%files -n %{python_pkg_name}-base
|
||||||
%defattr(644, root, root, 755)
|
|
||||||
# docs
|
# docs
|
||||||
%dir %{_docdir}/%{name}
|
%dir %{_docdir}/%{name}
|
||||||
%doc %{_docdir}/%{name}/README.rst
|
%doc %{_docdir}/%{name}/README.rst
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
From 7da97f61816f3cadaa6788804b22a2434b40e8c5 Mon Sep 17 00:00:00 2001
|
|
||||||
From: "Miss Islington (bot)"
|
|
||||||
<31488909+miss-islington@users.noreply.github.com>
|
|
||||||
Date: Mon, 21 Feb 2022 08:16:09 -0800
|
|
||||||
Subject: [PATCH] bpo-46811: Make test suite support Expat >=2.4.5 (GH-31453)
|
|
||||||
(GH-31472)
|
|
||||||
|
|
||||||
Curly brackets were never allowed in namespace URIs
|
|
||||||
according to RFC 3986, and so-called namespace-validating
|
|
||||||
XML parsers have the right to reject them a invalid URIs.
|
|
||||||
|
|
||||||
libexpat >=2.4.5 has become strcter in that regard due to
|
|
||||||
related security issues; with ET.XML instantiating a
|
|
||||||
namespace-aware parser under the hood, this test has no
|
|
||||||
future in CPython.
|
|
||||||
|
|
||||||
References:
|
|
||||||
- https://datatracker.ietf.org/doc/html/rfc3968
|
|
||||||
- https://www.w3.org/TR/xml-names/
|
|
||||||
|
|
||||||
Also, test_minidom.py: Support Expat >=2.4.5
|
|
||||||
(cherry picked from commit 2cae93832f46b245847bdc252456ddf7742ef45e)
|
|
||||||
|
|
||||||
Co-authored-by: Sebastian Pipping <sebastian@pipping.org>
|
|
||||||
---
|
|
||||||
Lib/test/test_minidom.py | 23 +++++++++--------------
|
|
||||||
1 file changed, 9 insertions(+), 14 deletions(-)
|
|
||||||
create mode 100644 Misc/NEWS.d/next/Library/2022-02-20-21-03-31.bpo-46811.8BxgdQ.rst
|
|
||||||
|
|
||||||
Index: Python-3.11.8/Lib/test/test_minidom.py
|
|
||||||
===================================================================
|
|
||||||
--- Python-3.11.8.orig/Lib/test/test_minidom.py
|
|
||||||
+++ Python-3.11.8/Lib/test/test_minidom.py
|
|
||||||
@@ -6,7 +6,6 @@ import io
|
|
||||||
from test import support
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
-import pyexpat
|
|
||||||
import xml.dom.minidom
|
|
||||||
|
|
||||||
from xml.dom.minidom import parse, Attr, Node, Document, parseString
|
|
||||||
@@ -1163,13 +1162,11 @@ class MinidomTest(unittest.TestCase):
|
|
||||||
|
|
||||||
# Verify that character decoding errors raise exceptions instead
|
|
||||||
# of crashing
|
|
||||||
- if pyexpat.version_info >= (2, 4, 5):
|
|
||||||
- self.assertRaises(ExpatError, parseString,
|
|
||||||
- b'<fran\xe7ais></fran\xe7ais>')
|
|
||||||
- self.assertRaises(ExpatError, parseString,
|
|
||||||
- b'<franais>Comment \xe7a va ? Tr\xe8s bien ?</franais>')
|
|
||||||
- else:
|
|
||||||
- self.assertRaises(UnicodeDecodeError, parseString,
|
|
||||||
+ # It doesn’t make any sense to insist on the exact text of the
|
|
||||||
+ # error message, or even the exact Exception … it is enough that
|
|
||||||
+ # the error has been discovered.
|
|
||||||
+ with self.assertRaises((UnicodeDecodeError, ExpatError)):
|
|
||||||
+ parseString(
|
|
||||||
b'<fran\xe7ais>Comment \xe7a va ? Tr\xe8s bien ?</fran\xe7ais>')
|
|
||||||
|
|
||||||
doc.unlink()
|
|
||||||
@@ -1631,12 +1628,10 @@ class MinidomTest(unittest.TestCase):
|
|
||||||
self.confirm(doc2.namespaceURI == xml.dom.EMPTY_NAMESPACE)
|
|
||||||
|
|
||||||
def testExceptionOnSpacesInXMLNSValue(self):
|
|
||||||
- if pyexpat.version_info >= (2, 4, 5):
|
|
||||||
- context = self.assertRaisesRegex(ExpatError, 'syntax error')
|
|
||||||
- else:
|
|
||||||
- context = self.assertRaisesRegex(ValueError, 'Unsupported syntax')
|
|
||||||
-
|
|
||||||
- with context:
|
|
||||||
+ # It doesn’t make any sense to insist on the exact text of the
|
|
||||||
+ # error message, or even the exact Exception … it is enough that
|
|
||||||
+ # the error has been discovered.
|
|
||||||
+ with self.assertRaises((ExpatError, ValueError)):
|
|
||||||
parseString('<element xmlns:abc="http:abc.com/de f g/hi/j k"><abc:foo /></element>')
|
|
||||||
|
|
||||||
def testDocRemoveChild(self):
|
|
Loading…
Reference in New Issue
Block a user