diff -Nuarp Python-3.14.3.orig/Lib/http/cookies.py Python-3.14.3/Lib/http/cookies.py
|
old
|
new
|
class Morsel(dict):
|
| 337 | 337 | key = key.lower() |
| 338 | 338 | if key not in self._reserved: |
| 339 | 339 | 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}") |
| 340 | 343 | data[key] = val |
| 341 | 344 | dict.update(self, data) |
| 342 | 345 | |
| | 346 | def __ior__(self, values): |
| | 347 | self.update(values) |
| | 348 | return self |
| | 349 | |
| 343 | 350 | def isReservedKey(self, K): |
| 344 | 351 | return K.lower() in self._reserved |
| 345 | 352 | |
| … |
… |
class Morsel(dict):
|
| 365 | 372 | } |
| 366 | 373 | |
| 367 | 374 | 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 |
| 371 | 384 | |
| 372 | 385 | def output(self, attrs=None, header="Set-Cookie:"): |
| 373 | 386 | return "%s %s" % (header, self.OutputString(attrs)) |
| … |
… |
class Morsel(dict):
|
| 379 | 392 | |
| 380 | 393 | def js_output(self, attrs=None): |
| 381 | 394 | # 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") |
| 382 | 398 | return """ |
| 383 | 399 | <script type="text/javascript"> |
| 384 | 400 | <!-- begin hiding |
| 385 | 401 | document.cookie = \"%s\"; |
| 386 | 402 | // end hiding --> |
| 387 | 403 | </script> |
| 388 | | """ % (self.OutputString(attrs).replace('"', r'\"')) |
| | 404 | """ % (output_string.replace('"', r'\"')) |
| 389 | 405 | |
| 390 | 406 | def OutputString(self, attrs=None): |
| 391 | 407 | # Build up our result |
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):
|
| 581 | 581 | with self.assertRaises(cookies.CookieError): |
| 582 | 582 | morsel["path"] = c0 |
| 583 | 583 | |
| | 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 | |
| 584 | 592 | # .setdefault() |
| 585 | 593 | with self.assertRaises(cookies.CookieError): |
| 586 | 594 | morsel.setdefault("path", c0) |
| … |
… |
class MorselTests(unittest.TestCase):
|
| 595 | 603 | with self.assertRaises(cookies.CookieError): |
| 596 | 604 | morsel.set("path", "val", c0) |
| 597 | 605 | |
| | 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 | |
| 598 | 618 | def test_control_characters_output(self): |
| 599 | 619 | # Tests that even if the internals of Morsel are modified |
| 600 | 620 | # that a call to .output() has control character safeguards. |
| … |
… |
class MorselTests(unittest.TestCase):
|
| 615 | 635 | with self.assertRaises(cookies.CookieError): |
| 616 | 636 | cookie.output() |
| 617 | 637 | |
| | 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 | |
| 618 | 656 | |
| 619 | 657 | def load_tests(loader, tests, pattern): |
| 620 | 658 | tests.addTest(doctest.DocTestSuite(cookies)) |
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
|
| 689 | 689 | parser.ElementDeclHandler = lambda _1, _2: None |
| 690 | 690 | self.assertRaises(TypeError, parser.Parse, data, True) |
| 691 | 691 | |
| | 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 | |
| 692 | 711 | class MalformedInputTest(unittest.TestCase): |
| 693 | 712 | def test1(self): |
| 694 | 713 | xml = b"\0\r\n" |
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
|
| 67 | 67 | options=[], |
| 68 | 68 | arguments=[URL]) |
| 69 | 69 | |
| | 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 | |
| 70 | 75 | |
| 71 | 76 | class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase): |
| 72 | 77 | |
diff -Nuarp Python-3.14.3.orig/Lib/webbrowser.py Python-3.14.3/Lib/webbrowser.py
|
old
|
new
|
class BaseBrowser:
|
| 163 | 163 | def open_new_tab(self, url): |
| 164 | 164 | return self.open(url, 2) |
| 165 | 165 | |
| | 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 | |
| 166 | 172 | |
| 167 | 173 | class GenericBrowser(BaseBrowser): |
| 168 | 174 | """Class for all browsers started with a command |
| … |
… |
class GenericBrowser(BaseBrowser):
|
| 180 | 186 | |
| 181 | 187 | def open(self, url, new=0, autoraise=True): |
| 182 | 188 | sys.audit("webbrowser.open", url) |
| | 189 | self._check_url(url) |
| 183 | 190 | cmdline = [self.name] + [arg.replace("%s", url) |
| 184 | 191 | for arg in self.args] |
| 185 | 192 | try: |
| … |
… |
class BackgroundBrowser(GenericBrowser):
|
| 200 | 207 | cmdline = [self.name] + [arg.replace("%s", url) |
| 201 | 208 | for arg in self.args] |
| 202 | 209 | sys.audit("webbrowser.open", url) |
| | 210 | self._check_url(url) |
| 203 | 211 | try: |
| 204 | 212 | if sys.platform[:3] == 'win': |
| 205 | 213 | p = subprocess.Popen(cmdline) |
| … |
… |
class UnixBrowser(BaseBrowser):
|
| 266 | 274 | |
| 267 | 275 | def open(self, url, new=0, autoraise=True): |
| 268 | 276 | sys.audit("webbrowser.open", url) |
| | 277 | self._check_url(url) |
| 269 | 278 | if new == 0: |
| 270 | 279 | action = self.remote_action |
| 271 | 280 | elif new == 1: |
| … |
… |
class Konqueror(BaseBrowser):
|
| 357 | 366 | |
| 358 | 367 | def open(self, url, new=0, autoraise=True): |
| 359 | 368 | sys.audit("webbrowser.open", url) |
| | 369 | self._check_url(url) |
| 360 | 370 | # XXX Currently I know no way to prevent KFM from opening a new win. |
| 361 | 371 | if new == 2: |
| 362 | 372 | action = "newTab" |
| … |
… |
if sys.platform[:3] == "win":
|
| 588 | 598 | class WindowsDefault(BaseBrowser): |
| 589 | 599 | def open(self, url, new=0, autoraise=True): |
| 590 | 600 | sys.audit("webbrowser.open", url) |
| | 601 | self._check_url(url) |
| 591 | 602 | try: |
| 592 | 603 | os.startfile(url) |
| 593 | 604 | except OSError: |
| … |
… |
if sys.platform == 'darwin':
|
| 608 | 619 | |
| 609 | 620 | def open(self, url, new=0, autoraise=True): |
| 610 | 621 | sys.audit("webbrowser.open", url) |
| | 622 | self._check_url(url) |
| 611 | 623 | url = url.replace('"', '%22') |
| 612 | 624 | if self.name == 'default': |
| 613 | 625 | proto, _sep, _rest = url.partition(":") |
| … |
… |
if sys.platform == "ios":
|
| 664 | 676 | class IOSBrowser(BaseBrowser): |
| 665 | 677 | def open(self, url, new=0, autoraise=True): |
| 666 | 678 | sys.audit("webbrowser.open", url) |
| | 679 | self._check_url(url) |
| 667 | 680 | # If ctypes isn't available, we can't open a browser |
| 668 | 681 | if objc is None: |
| 669 | 682 | return False |
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
|
|
| | 1 | Reject leading dashes in URLs passed to :func:`webbrowser.open` |
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
|
|
| | 1 | Reject control characters in :class:`http.cookies.Morsel` |
| | 2 | :meth:`~http.cookies.Morsel.update` and |
| | 3 | :meth:`~http.cookies.BaseCookie.js_output`. |
| | 4 | This addresses :cve:`2026-3644`. |
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 |
| | 2 | converting deeply nested XML content models with |
| | 3 | :meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`. |
| | 4 | This addresses :cve:`2026-4224`. |
diff -Nuarp Python-3.14.3.orig/Modules/pyexpat.c Python-3.14.3/Modules/pyexpat.c
|
old
|
new
|
|
| 3 | 3 | #endif |
| 4 | 4 | |
| 5 | 5 | #include "Python.h" |
| | 6 | #include "pycore_ceval.h" // _Py_EnterRecursiveCall() |
| 6 | 7 | #include "pycore_import.h" // _PyImport_SetModule() |
| 7 | 8 | #include "pycore_pyhash.h" // _Py_HashSecret |
| 8 | 9 | #include "pycore_traceback.h" // _PyTraceback_Add() |
| … |
… |
static PyObject *
|
| 603 | 604 | conv_content_model(XML_Content * const model, |
| 604 | 605 | PyObject *(*conv_string)(void *)) |
| 605 | 606 | { |
| | 607 | if (_Py_EnterRecursiveCall(" in conv_content_model")) { |
| | 608 | return NULL; |
| | 609 | } |
| | 610 | |
| 606 | 611 | PyObject *result = NULL; |
| 607 | 612 | PyObject *children = PyTuple_New(model->numchildren); |
| 608 | 613 | int i; |
| … |
… |
conv_content_model(XML_Content * const m
|
| 614 | 619 | conv_string); |
| 615 | 620 | if (child == NULL) { |
| 616 | 621 | Py_XDECREF(children); |
| 617 | | return NULL; |
| | 622 | goto done; |
| 618 | 623 | } |
| 619 | 624 | PyTuple_SET_ITEM(children, i, child); |
| 620 | 625 | } |
| … |
… |
conv_content_model(XML_Content * const m
|
| 622 | 627 | model->type, model->quant, |
| 623 | 628 | conv_string, model->name, children); |
| 624 | 629 | } |
| | 630 | done: |
| | 631 | _Py_LeaveRecursiveCall(); |
| 625 | 632 | return result; |
| 626 | 633 | } |
| 627 | 634 | |