From: Theo Buehler Subject: Re: python 3.12.13 update To: Kurt Mosiejczuk , ports Date: Tue, 17 Mar 2026 14:52:01 +0100 On Mon, Mar 16, 2026 at 11:30:09PM +0000, Stuart Henderson wrote: > On 2026/03/16 23:23, Stuart Henderson wrote: > > This updates to February's release, and cherry-picks two recent CVE > > fixes (med/high). I expect another release will be on the way at some > > point but seems worth bringing them in already. > > > > Diff to follow for 7.8-stable. > > There's no commit on the 3.12 branch for those CVEs yet; probably > doesn't need a lot of work but for now here's the 3.12.13 update with > the required bits to cope with our expat build. Here's the diff with the backports from the still not merged https://github.com/python/cpython/pull/146025 https://github.com/python/cpython/pull/145999 The relevant regress tests pass. This one's also ok by me Index: Makefile =================================================================== RCS file: /cvs/ports/lang/python/3/Makefile,v diff -u -p -r1.15 Makefile --- Makefile 14 Jun 2025 20:08:20 -0000 1.15 +++ Makefile 17 Mar 2026 13:49:57 -0000 @@ -3,12 +3,15 @@ # requirement of the PSF license, if it constitutes a change to # Python itself. -FULL_VERSION = 3.12.11 +FULL_VERSION = 3.12.13 SHARED_LIBS = python3.12 0.0 VERSION_SPEC = >=3.12,<3.13 PORTROACH = limit:^3\.12 AUTOCONF_VERSION = 2.71 + +# XXX - should come from expat_config.h, which isn't installed in base +CFLAGS += -DXML_DTD -DXML_GE=1 CONFIGURE_ENV += ac_cv_working_openssl_hashlib=yes \ TCLTK_CFLAGS="-I${MODTCL_INCDIR} -I${MODTK_INCDIR} -I${X11BASE}/include" \ Index: distinfo =================================================================== RCS file: /cvs/ports/lang/python/3/distinfo,v diff -u -p -r1.6 distinfo --- distinfo 14 Jun 2025 20:08:20 -0000 1.6 +++ distinfo 17 Mar 2026 13:49:57 -0000 @@ -1,2 +1,2 @@ -SHA256 (Python-3.12.11.tgz) = e41Zr4IWBE0jE96BIL/CzACpvS5ULxV5Xh1hbFH689Y= -SIZE (Python-3.12.11.tgz) = 27126512 +SHA256 (Python-3.12.13.tgz) = CBbEdhyX7Ns/UKOSTeCpP9eMtj7o5sBCAd367cpQCws= +SIZE (Python-3.12.13.tgz) = 27262093 Index: files/CHANGES.OpenBSD =================================================================== RCS file: /cvs/ports/lang/python/3/files/CHANGES.OpenBSD,v diff -u -p -r1.2 CHANGES.OpenBSD --- files/CHANGES.OpenBSD 21 Dec 2024 11:32:44 -0000 1.2 +++ files/CHANGES.OpenBSD 17 Mar 2026 13:49:57 -0000 @@ -25,5 +25,9 @@ which results in loading an incorrect ve 8. Adjust #ifdef to use LibreSSL's version of X509_STORE_get1_objects(). +9. Work around expat_config.h missing from base. + +10. Cherry-pick fixes for CVE-2026-3644, CVE-2026-4224 + These changes are available in the OpenBSD CVS repository in ports/lang/python/3. Index: patches/patch-Lib_http_cookies_py =================================================================== RCS file: patches/patch-Lib_http_cookies_py diff -N patches/patch-Lib_http_cookies_py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-Lib_http_cookies_py 17 Mar 2026 13:49:57 -0000 @@ -0,0 +1,73 @@ +https://github.com/python/cpython/pull/146025 + +From 736de53d3d91e83e9ff8a5d81b418bd8ea033e35 Mon Sep 17 00:00:00 2001 +From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> +Date: Mon, 16 Mar 2026 13:43:43 +0000 +Subject: [PATCH] gh-145599, CVE 2026-3644: Reject control characters in + `http.cookies.Morsel.update()` (GH-145600) + +Reject control characters in `http.cookies.Morsel.update()` and `http.cookies.BaseCookie.js_outpu +t`. + +Reject control characters in :class:`http.cookies.Morsel` +:meth:`~http.cookies.Morsel.update` and +:meth:`~http.cookies.BaseCookie.js_output`. +This addresses :cve:`2026-3644`. + +Index: Lib/http/cookies.py +--- Lib/http/cookies.py.orig ++++ Lib/http/cookies.py +@@ -335,9 +335,16 @@ class Morsel(dict): + key = key.lower() + if key not in self._reserved: + raise CookieError("Invalid attribute %r" % (key,)) ++ if _has_control_character(key, val): ++ raise CookieError("Control characters are not allowed in " ++ f"cookies {key!r} {val!r}") + data[key] = val + dict.update(self, data) + ++ def __ior__(self, values): ++ self.update(values) ++ return self ++ + def isReservedKey(self, K): + return K.lower() in self._reserved + +@@ -363,9 +370,15 @@ class Morsel(dict): + } + + def __setstate__(self, state): +- self._key = state['key'] +- self._value = state['value'] +- self._coded_value = state['coded_value'] ++ key = state['key'] ++ value = state['value'] ++ coded_value = state['coded_value'] ++ if _has_control_character(key, value, coded_value): ++ raise CookieError("Control characters are not allowed in cookies " ++ f"{key!r} {value!r} {coded_value!r}") ++ self._key = key ++ self._value = value ++ self._coded_value = coded_value + + def output(self, attrs=None, header="Set-Cookie:"): + return "%s %s" % (header, self.OutputString(attrs)) +@@ -377,13 +390,16 @@ class Morsel(dict): + + def js_output(self, attrs=None): + # Print javascript ++ output_string = self.OutputString(attrs) ++ if _has_control_character(output_string): ++ raise CookieError("Control characters are not allowed in cookies") + return """ + +- """ % (self.OutputString(attrs).replace('"', r'\"')) ++ """ % (output_string.replace('"', r'\"')) + + def OutputString(self, attrs=None): + # Build up our result Index: patches/patch-Lib_test_test_http_cookies_py =================================================================== RCS file: patches/patch-Lib_test_test_http_cookies_py diff -N patches/patch-Lib_test_test_http_cookies_py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-Lib_test_test_http_cookies_py 17 Mar 2026 13:49:57 -0000 @@ -0,0 +1,78 @@ +https://github.com/python/cpython/pull/146025 + +From 736de53d3d91e83e9ff8a5d81b418bd8ea033e35 Mon Sep 17 00:00:00 2001 +From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> +Date: Mon, 16 Mar 2026 13:43:43 +0000 +Subject: [PATCH] gh-145599, CVE 2026-3644: Reject control characters in + `http.cookies.Morsel.update()` (GH-145600) + +Reject control characters in `http.cookies.Morsel.update()` and `http.cookies.BaseCookie.js_outpu +t`. + +Reject control characters in :class:`http.cookies.Morsel` +:meth:`~http.cookies.Morsel.update` and +:meth:`~http.cookies.BaseCookie.js_output`. +This addresses :cve:`2026-3644`. + +Index: Lib/test/test_http_cookies.py +--- Lib/test/test_http_cookies.py.orig ++++ Lib/test/test_http_cookies.py +@@ -573,6 +573,14 @@ class MorselTests(unittest.TestCase): + with self.assertRaises(cookies.CookieError): + morsel["path"] = c0 + ++ # .__setstate__() ++ with self.assertRaises(cookies.CookieError): ++ morsel.__setstate__({'key': c0, 'value': 'val', 'coded_value': 'coded'}) ++ with self.assertRaises(cookies.CookieError): ++ morsel.__setstate__({'key': 'key', 'value': c0, 'coded_value': 'coded'}) ++ with self.assertRaises(cookies.CookieError): ++ morsel.__setstate__({'key': 'key', 'value': 'val', 'coded_value': c0}) ++ + # .setdefault() + with self.assertRaises(cookies.CookieError): + morsel.setdefault("path", c0) +@@ -587,6 +595,18 @@ class MorselTests(unittest.TestCase): + with self.assertRaises(cookies.CookieError): + morsel.set("path", "val", c0) + ++ # .update() ++ with self.assertRaises(cookies.CookieError): ++ morsel.update({"path": c0}) ++ with self.assertRaises(cookies.CookieError): ++ morsel.update({c0: "val"}) ++ ++ # .__ior__() ++ with self.assertRaises(cookies.CookieError): ++ morsel |= {"path": c0} ++ with self.assertRaises(cookies.CookieError): ++ morsel |= {c0: "val"} ++ + def test_control_characters_output(self): + # Tests that even if the internals of Morsel are modified + # that a call to .output() has control character safeguards. +@@ -606,6 +626,24 @@ class MorselTests(unittest.TestCase): + cookie["cookie"] = morsel + with self.assertRaises(cookies.CookieError): + cookie.output() ++ ++ # Tests that .js_output() also has control character safeguards. ++ for c0 in support.control_characters_c0(): ++ morsel = cookies.Morsel() ++ morsel.set("key", "value", "coded-value") ++ morsel._key = c0 # Override private variable. ++ cookie = cookies.SimpleCookie() ++ cookie["cookie"] = morsel ++ with self.assertRaises(cookies.CookieError): ++ cookie.js_output() ++ ++ morsel = cookies.Morsel() ++ morsel.set("key", "value", "coded-value") ++ morsel._coded_value = c0 # Override private variable. ++ cookie = cookies.SimpleCookie() ++ cookie["cookie"] = morsel ++ with self.assertRaises(cookies.CookieError): ++ cookie.js_output() + + + def load_tests(loader, tests, pattern): Index: patches/patch-Lib_test_test_pyexpat_py =================================================================== RCS file: patches/patch-Lib_test_test_pyexpat_py diff -N patches/patch-Lib_test_test_pyexpat_py --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-Lib_test_test_pyexpat_py 17 Mar 2026 13:49:57 -0000 @@ -0,0 +1,41 @@ +https://github.com/python/cpython/pull/145999 + +From e5caf45faac74b0ed869e3336420cffd3510ce6e Mon Sep 17 00:00:00 2001 +From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> +Date: Sun, 15 Mar 2026 21:46:06 +0000 +Subject: [PATCH] [3.12] gh-145986: Avoid unbound C recursion in + `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) + +:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when +converting deeply nested XML content models with +:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`. +This addresses :cve:`2026-4224`. + +Index: Lib/test/test_pyexpat.py +--- Lib/test/test_pyexpat.py.orig ++++ Lib/test/test_pyexpat.py +@@ -675,6 +675,24 @@ class ChardataBufferTest(unittest.TestCase): + parser.Parse(xml2, True) + self.assertEqual(self.n, 4) + ++class ElementDeclHandlerTest(unittest.TestCase): ++ def test_deeply_nested_content_model(self): ++ # This should raise a RecursionError and not crash. ++ # See https://github.com/python/cpython/issues/145986. ++ N = 500_000 ++ data = ( ++ b'\n]>\n\n' ++ ) ++ ++ parser = expat.ParserCreate() ++ parser.ElementDeclHandler = lambda _1, _2: None ++ with support.infinite_recursion(): ++ with self.assertRaises(RecursionError): ++ parser.Parse(data) ++ ++ + class MalformedInputTest(unittest.TestCase): + def test1(self): + xml = b"\0\r\n" Index: patches/patch-Modules_pyexpat_c =================================================================== RCS file: patches/patch-Modules_pyexpat_c diff -N patches/patch-Modules_pyexpat_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-Modules_pyexpat_c 17 Mar 2026 13:49:57 -0000 @@ -0,0 +1,63 @@ +- remove #include "expat_config.h"; this (or an alternative, see +textproc/xmlwf for another approach) needs to be kept unless expat +in base starts providing this config header + +- https://github.com/python/cpython/pull/145999 + +From e5caf45faac74b0ed869e3336420cffd3510ce6e Mon Sep 17 00:00:00 2001 +From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> +Date: Sun, 15 Mar 2026 21:46:06 +0000 +Subject: [PATCH] [3.12] gh-145986: Avoid unbound C recursion in + `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) + +:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when +converting deeply nested XML content models with +:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`. +This addresses :cve:`2026-4224`. + +Index: Modules/pyexpat.c +--- Modules/pyexpat.c.orig ++++ Modules/pyexpat.c +@@ -3,12 +3,12 @@ + #endif + + #include "Python.h" ++#include "pycore_ceval.h" // _Py_EnterRecursiveCall() + #include "pycore_runtime.h" // _Py_ID() + #include + + #include + #include "structmember.h" // PyMemberDef +-#include "expat_config.h" + #include "expat.h" + + #include "pyexpat.h" +@@ -578,6 +578,10 @@ static PyObject * + conv_content_model(XML_Content * const model, + PyObject *(*conv_string)(const XML_Char *)) + { ++ if (_Py_EnterRecursiveCall(" in conv_content_model")) { ++ return NULL; ++ } ++ + PyObject *result = NULL; + PyObject *children = PyTuple_New(model->numchildren); + int i; +@@ -589,7 +593,7 @@ conv_content_model(XML_Content * const model, + conv_string); + if (child == NULL) { + Py_XDECREF(children); +- return NULL; ++ goto done; + } + PyTuple_SET_ITEM(children, i, child); + } +@@ -597,6 +601,8 @@ conv_content_model(XML_Content * const model, + model->type, model->quant, + conv_string,model->name, children); + } ++done: ++ _Py_LeaveRecursiveCall(); + return result; + } + Index: pkg/PLIST-tests =================================================================== RCS file: /cvs/ports/lang/python/3/pkg/PLIST-tests,v diff -u -p -r1.8 PLIST-tests --- pkg/PLIST-tests 15 Apr 2025 20:36:43 -0000 1.8 +++ pkg/PLIST-tests 17 Mar 2026 13:49:57 -0000 @@ -4820,7 +4820,7 @@ lib/python3.12/test/typinganndata/ann_mo lib/python3.12/test/typinganndata/ann_module9.py lib/python3.12/test/typinganndata/mod_generics_cache.py lib/python3.12/test/wheeldata/ -lib/python3.12/test/wheeldata/setuptools-67.6.1-py3-none-any.whl +lib/python3.12/test/wheeldata/setuptools-79.0.1-py3-none-any.whl lib/python3.12/test/wheeldata/wheel-0.40.0-py3-none-any.whl lib/python3.12/test/win_console_handler.py lib/python3.12/test/xmltestdata/