Ticket #5896: python-3.14.3-security_fixes.patch

File python-3.14.3-security_fixes.patch, 12.9 KB (added by Joe Locash, 3 hours ago)
  • Lib/http/cookies.py

    diff -Nuarp Python-3.14.3.orig/Lib/http/cookies.py Python-3.14.3/Lib/http/cookies.py
    old new class Morsel(dict):  
    337337            key = key.lower()
    338338            if key not in self._reserved:
    339339                raise CookieError("Invalid attribute %r" % (key,))
     340            if _has_control_character(key, val):
     341                raise CookieError("Control characters are not allowed in "
     342                                  f"cookies {key!r} {val!r}")
    340343            data[key] = val
    341344        dict.update(self, data)
    342345
     346    def __ior__(self, values):
     347        self.update(values)
     348        return self
     349
    343350    def isReservedKey(self, K):
    344351        return K.lower() in self._reserved
    345352
    class Morsel(dict):  
    365372        }
    366373
    367374    def __setstate__(self, state):
    368         self._key = state['key']
    369         self._value = state['value']
    370         self._coded_value = state['coded_value']
     375        key = state['key']
     376        value = state['value']
     377        coded_value = state['coded_value']
     378        if _has_control_character(key, value, coded_value):
     379            raise CookieError("Control characters are not allowed in cookies "
     380                              f"{key!r} {value!r} {coded_value!r}")
     381        self._key = key
     382        self._value = value
     383        self._coded_value = coded_value
    371384
    372385    def output(self, attrs=None, header="Set-Cookie:"):
    373386        return "%s %s" % (header, self.OutputString(attrs))
    class Morsel(dict):  
    379392
    380393    def js_output(self, attrs=None):
    381394        # Print javascript
     395        output_string = self.OutputString(attrs)
     396        if _has_control_character(output_string):
     397            raise CookieError("Control characters are not allowed in cookies")
    382398        return """
    383399        <script type="text/javascript">
    384400        <!-- begin hiding
    385401        document.cookie = \"%s\";
    386402        // end hiding -->
    387403        </script>
    388         """ % (self.OutputString(attrs).replace('"', r'\"'))
     404        """ % (output_string.replace('"', r'\"'))
    389405
    390406    def OutputString(self, attrs=None):
    391407        # Build up our result
  • Lib/test/test_http_cookies.py

    diff -Nuarp Python-3.14.3.orig/Lib/test/test_http_cookies.py Python-3.14.3/Lib/test/test_http_cookies.py
    old new class MorselTests(unittest.TestCase):  
    581581            with self.assertRaises(cookies.CookieError):
    582582                morsel["path"] = c0
    583583
     584            # .__setstate__()
     585            with self.assertRaises(cookies.CookieError):
     586                morsel.__setstate__({'key': c0, 'value': 'val', 'coded_value': 'coded'})
     587            with self.assertRaises(cookies.CookieError):
     588                morsel.__setstate__({'key': 'key', 'value': c0, 'coded_value': 'coded'})
     589            with self.assertRaises(cookies.CookieError):
     590                morsel.__setstate__({'key': 'key', 'value': 'val', 'coded_value': c0})
     591
    584592            # .setdefault()
    585593            with self.assertRaises(cookies.CookieError):
    586594                morsel.setdefault("path", c0)
    class MorselTests(unittest.TestCase):  
    595603            with self.assertRaises(cookies.CookieError):
    596604                morsel.set("path", "val", c0)
    597605
     606            # .update()
     607            with self.assertRaises(cookies.CookieError):
     608                morsel.update({"path": c0})
     609            with self.assertRaises(cookies.CookieError):
     610                morsel.update({c0: "val"})
     611
     612            # .__ior__()
     613            with self.assertRaises(cookies.CookieError):
     614                morsel |= {"path": c0}
     615            with self.assertRaises(cookies.CookieError):
     616                morsel |= {c0: "val"}
     617
    598618    def test_control_characters_output(self):
    599619        # Tests that even if the internals of Morsel are modified
    600620        # that a call to .output() has control character safeguards.
    class MorselTests(unittest.TestCase):  
    615635            with self.assertRaises(cookies.CookieError):
    616636                cookie.output()
    617637
     638        # Tests that .js_output() also has control character safeguards.
     639        for c0 in support.control_characters_c0():
     640            morsel = cookies.Morsel()
     641            morsel.set("key", "value", "coded-value")
     642            morsel._key = c0  # Override private variable.
     643            cookie = cookies.SimpleCookie()
     644            cookie["cookie"] = morsel
     645            with self.assertRaises(cookies.CookieError):
     646                cookie.js_output()
     647
     648            morsel = cookies.Morsel()
     649            morsel.set("key", "value", "coded-value")
     650            morsel._coded_value = c0  # Override private variable.
     651            cookie = cookies.SimpleCookie()
     652            cookie["cookie"] = morsel
     653            with self.assertRaises(cookies.CookieError):
     654                cookie.js_output()
     655
    618656
    619657def load_tests(loader, tests, pattern):
    620658    tests.addTest(doctest.DocTestSuite(cookies))
  • Lib/test/test_pyexpat.py

    diff -Nuarp Python-3.14.3.orig/Lib/test/test_pyexpat.py Python-3.14.3/Lib/test/test_pyexpat.py
    old new class ElementDeclHandlerTest(unittest.Te  
    689689        parser.ElementDeclHandler = lambda _1, _2: None
    690690        self.assertRaises(TypeError, parser.Parse, data, True)
    691691
     692    @support.skip_if_unlimited_stack_size
     693    @support.skip_emscripten_stack_overflow()
     694    @support.skip_wasi_stack_overflow()
     695    def test_deeply_nested_content_model(self):
     696        # This should raise a RecursionError and not crash.
     697        # See https://github.com/python/cpython/issues/145986.
     698        N = 500_000
     699        data = (
     700            b'<!DOCTYPE root [\n<!ELEMENT root '
     701            + b'(a, ' * N + b'a' + b')' * N
     702            + b'>\n]>\n<root/>\n'
     703        )
     704
     705        parser = expat.ParserCreate()
     706        parser.ElementDeclHandler = lambda _1, _2: None
     707        with support.infinite_recursion():
     708            with self.assertRaises(RecursionError):
     709                parser.Parse(data)
     710
    692711class MalformedInputTest(unittest.TestCase):
    693712    def test1(self):
    694713        xml = b"\0\r\n"
  • Lib/test/test_webbrowser.py

    diff -Nuarp Python-3.14.3.orig/Lib/test/test_webbrowser.py Python-3.14.3/Lib/test/test_webbrowser.py
    old new class GenericBrowserCommandTest(CommandT  
    6767                   options=[],
    6868                   arguments=[URL])
    6969
     70    def test_reject_dash_prefixes(self):
     71        browser = self.browser_class(name=CMD_NAME)
     72        with self.assertRaises(ValueError):
     73            browser.open(f"--key=val {URL}")
     74
    7075
    7176class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase):
    7277
  • Lib/webbrowser.py

    diff -Nuarp Python-3.14.3.orig/Lib/webbrowser.py Python-3.14.3/Lib/webbrowser.py
    old new class BaseBrowser:  
    163163    def open_new_tab(self, url):
    164164        return self.open(url, 2)
    165165
     166    @staticmethod
     167    def _check_url(url):
     168        """Ensures that the URL is safe to pass to subprocesses as a parameter"""
     169        if url and url.lstrip().startswith("-"):
     170            raise ValueError(f"Invalid URL: {url}")
     171
    166172
    167173class GenericBrowser(BaseBrowser):
    168174    """Class for all browsers started with a command
    class GenericBrowser(BaseBrowser):  
    180186
    181187    def open(self, url, new=0, autoraise=True):
    182188        sys.audit("webbrowser.open", url)
     189        self._check_url(url)
    183190        cmdline = [self.name] + [arg.replace("%s", url)
    184191                                 for arg in self.args]
    185192        try:
    class BackgroundBrowser(GenericBrowser):  
    200207        cmdline = [self.name] + [arg.replace("%s", url)
    201208                                 for arg in self.args]
    202209        sys.audit("webbrowser.open", url)
     210        self._check_url(url)
    203211        try:
    204212            if sys.platform[:3] == 'win':
    205213                p = subprocess.Popen(cmdline)
    class UnixBrowser(BaseBrowser):  
    266274
    267275    def open(self, url, new=0, autoraise=True):
    268276        sys.audit("webbrowser.open", url)
     277        self._check_url(url)
    269278        if new == 0:
    270279            action = self.remote_action
    271280        elif new == 1:
    class Konqueror(BaseBrowser):  
    357366
    358367    def open(self, url, new=0, autoraise=True):
    359368        sys.audit("webbrowser.open", url)
     369        self._check_url(url)
    360370        # XXX Currently I know no way to prevent KFM from opening a new win.
    361371        if new == 2:
    362372            action = "newTab"
    if sys.platform[:3] == "win":  
    588598    class WindowsDefault(BaseBrowser):
    589599        def open(self, url, new=0, autoraise=True):
    590600            sys.audit("webbrowser.open", url)
     601            self._check_url(url)
    591602            try:
    592603                os.startfile(url)
    593604            except OSError:
    if sys.platform == 'darwin':  
    608619
    609620        def open(self, url, new=0, autoraise=True):
    610621            sys.audit("webbrowser.open", url)
     622            self._check_url(url)
    611623            url = url.replace('"', '%22')
    612624            if self.name == 'default':
    613625                proto, _sep, _rest = url.partition(":")
    if sys.platform == "ios":  
    664676    class IOSBrowser(BaseBrowser):
    665677        def open(self, url, new=0, autoraise=True):
    666678            sys.audit("webbrowser.open", url)
     679            self._check_url(url)
    667680            # If ctypes isn't available, we can't open a browser
    668681            if objc is None:
    669682                return False
  • Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst

    diff -Nuarp Python-3.14.3.orig/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst Python-3.14.3/Misc/NEWS.d/next/Security/2026-01-16-12-04-49.gh-issue-143930.zYC5x3.rst
    old new  
     1Reject leading dashes in URLs passed to :func:`webbrowser.open`
  • Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst

    diff -Nuarp Python-3.14.3.orig/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst Python-3.14.3/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst
    old new  
     1Reject control characters in :class:`http.cookies.Morsel`
     2:meth:`~http.cookies.Morsel.update` and
     3:meth:`~http.cookies.BaseCookie.js_output`.
     4This addresses :cve:`2026-3644`.
  • Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst

    diff -Nuarp Python-3.14.3.orig/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst Python-3.14.3/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst
    old new  
     1:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when
     2converting deeply nested XML content models with
     3:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`.
     4This addresses :cve:`2026-4224`.
  • Modules/pyexpat.c

    diff -Nuarp Python-3.14.3.orig/Modules/pyexpat.c Python-3.14.3/Modules/pyexpat.c
    old new  
    33#endif
    44
    55#include "Python.h"
     6#include "pycore_ceval.h"         // _Py_EnterRecursiveCall()
    67#include "pycore_import.h"        // _PyImport_SetModule()
    78#include "pycore_pyhash.h"        // _Py_HashSecret
    89#include "pycore_traceback.h"     // _PyTraceback_Add()
    static PyObject *  
    603604conv_content_model(XML_Content * const model,
    604605                   PyObject *(*conv_string)(void *))
    605606{
     607    if (_Py_EnterRecursiveCall(" in conv_content_model")) {
     608        return NULL;
     609    }
     610
    606611    PyObject *result = NULL;
    607612    PyObject *children = PyTuple_New(model->numchildren);
    608613    int i;
    conv_content_model(XML_Content * const m  
    614619                                                 conv_string);
    615620            if (child == NULL) {
    616621                Py_XDECREF(children);
    617                 return NULL;
     622                goto done;
    618623            }
    619624            PyTuple_SET_ITEM(children, i, child);
    620625        }
    conv_content_model(XML_Content * const m  
    622627                               model->type, model->quant,
    623628                               conv_string, model->name, children);
    624629    }
     630done:
     631    _Py_LeaveRecursiveCall();
    625632    return result;
    626633}
    627634