Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-tornado6 for openSUSE:Factory 
checked in at 2026-03-14 22:20:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-tornado6 (Old)
 and      /work/SRC/openSUSE:Factory/.python-tornado6.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-tornado6"

Sat Mar 14 22:20:43 2026 rev:22 rq:1338684 version:6.5.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-tornado6/python-tornado6.changes  
2025-12-20 21:45:08.635851013 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-tornado6.new.8177/python-tornado6.changes    
    2026-03-14 22:21:10.218087682 +0100
@@ -1,0 +2,19 @@
+Thu Mar 12 12:39:23 UTC 2026 - Nico Krapp <[email protected]>
+
+- Update to 6.5.5 (CVE-2026-31958, bsc#1259553)
+  * ``multipart/form-data`` requests are now limited to 100 parts by default,
+    to prevent a denial-of-service attack via very large requests with many
+    parts. This limit is configurable via
+    `tornado.httputil.ParseMultipartConfig`. Multipart parsing can also be
+    disabled completely if not required for the application.
+    Thanks to 0x-Apollyon and bekkaze for reporting this issue
+  * The ``domain``, ``path``, and ``samesite`` arguments to
+    `.RequestHandler.set_cookie` are now validated for illegal characters, 
which
+    could be abused to inject other attributes on the cookie.
+    Thanks to Dhiral Vyas (Praetorian) for reporting this issue.
+  * Carriage return characters are no longer accepted in 
``multipart/form-data``
+    headers.
+    Thanks to sergeykochanov for reporting this issue.
+- add fix-tests-with-curl-8-19.patch to fix tests with curl 8.19
+
+-------------------------------------------------------------------

Old:
----
  tornado-6.5.4.tar.gz

New:
----
  fix-tests-with-curl-8-19.patch
  tornado-6.5.5.tar.gz

----------(New B)----------
  New:    Thanks to sergeykochanov for reporting this issue.
- add fix-tests-with-curl-8-19.patch to fix tests with curl 8.19
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-tornado6.spec ++++++
--- /var/tmp/diff_new_pack.y2lFsd/_old  2026-03-14 22:21:11.162126761 +0100
+++ /var/tmp/diff_new_pack.y2lFsd/_new  2026-03-14 22:21:11.166126927 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-tornado6
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-tornado6
-Version:        6.5.4
+Version:        6.5.5
 Release:        0
 Summary:        Open source version of scalable, non-blocking web server that 
power FriendFeed
 License:        Apache-2.0
@@ -27,6 +27,8 @@
 Source99:       python-tornado6-rpmlintrc
 # PATCH-FIX-OPENSUSE ignore-resourcewarning-doctests.patch -- ignore resource 
warnings on OBS
 Patch0:         ignore-resourcewarning-doctests.patch
+# PATCH-FIX-UPSTREAM fix-tests-with-curl-8-19.patch 
gh#tornadoweb/tornado@de5e943
+Patch1:         fix-tests-with-curl-8-19.patch
 BuildRequires:  %{python_module base >= 3.8}
 BuildRequires:  %{python_module devel}
 BuildRequires:  %{python_module pip}

++++++ fix-tests-with-curl-8-19.patch ++++++
>From de5e9432fb10f7081db089ec1ab75d1a48ab2772 Mon Sep 17 00:00:00 2001
From: Carlos Henrique Lima Melara <[email protected]>
Date: Fri, 6 Mar 2026 00:56:17 -0300
Subject: [PATCH] Make tests compatible with curl 8.19.0

In 8.19.0-rc2, the error logic has been changed so any later errors are
preserved. This changes what is returned by curl and therefore what tornado
sees. For HTTPError variant of the test, which uses CurlAsyncHTTPClient, we get
the error from pycurl and now it contains "Failed binding local connection
end". This logic handles both the old version of libcurl and also the newer
one.

Co-Authored-By: Samuel Henrique <[email protected]>
---
 tornado/test/httpclient_test.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
index 77c0d6eb9b..caed23e045 100644
--- a/tornado/test/httpclient_test.py
+++ b/tornado/test/httpclient_test.py
@@ -622,7 +622,10 @@ def test_bind_source_ip(self):
         with self.assertRaises((ValueError, HTTPError)) as context:  # type: 
ignore
             request = HTTPRequest(url, network_interface="not-interface-or-ip")
             yield self.http_client.fetch(request)
-        self.assertIn("not-interface-or-ip", str(context.exception))
+        self.assertTrue(
+            "Failed binding local connection end" in str(context.exception)
+            or "not-interface-or-ip" in str(context.exception)
+        )
 
     def test_all_methods(self):
         for method in ["GET", "DELETE", "OPTIONS"]:


++++++ ignore-resourcewarning-doctests.patch ++++++
--- /var/tmp/diff_new_pack.y2lFsd/_old  2026-03-14 22:21:11.206128583 +0100
+++ /var/tmp/diff_new_pack.y2lFsd/_new  2026-03-14 22:21:11.210128748 +0100
@@ -1,8 +1,8 @@
-Index: tornado-6.0.4/tornado/util.py
+Index: tornado-6.5.5/tornado/util.py
 ===================================================================
---- tornado-6.0.4.orig/tornado/util.py 2020-03-11 11:42:49.610254636 +0100
-+++ tornado-6.0.4/tornado/util.py      2020-03-11 11:43:51.470603323 +0100
-@@ -468,5 +468,7 @@ else:
+--- tornado-6.5.5.orig/tornado/util.py
++++ tornado-6.5.5/tornado/util.py
+@@ -441,5 +441,7 @@ else:
  def doctests():
      # type: () -> unittest.TestSuite
      import doctest
@@ -10,24 +10,24 @@
 +    warnings.simplefilter("ignore", ResourceWarning)
  
      return doctest.DocTestSuite()
-Index: tornado-6.0.4/tornado/httputil.py
+Index: tornado-6.5.5/tornado/httputil.py
 ===================================================================
---- tornado-6.0.4.orig/tornado/httputil.py     2020-03-11 11:42:49.610254636 
+0100
-+++ tornado-6.0.4/tornado/httputil.py  2020-03-11 11:44:46.178911693 +0100
-@@ -1032,6 +1032,8 @@ def encode_username_password(
+--- tornado-6.5.5.orig/tornado/httputil.py
++++ tornado-6.5.5/tornado/httputil.py
+@@ -1295,6 +1295,8 @@ def encode_username_password(
  def doctests():
      # type: () -> unittest.TestSuite
      import doctest
 +    import warnings
 +    warnings.simplefilter("ignore", ResourceWarning)
  
-     return doctest.DocTestSuite()
+     return doctest.DocTestSuite(optionflags=doctest.ELLIPSIS)
  
-Index: tornado-6.0.4/tornado/iostream.py
+Index: tornado-6.5.5/tornado/iostream.py
 ===================================================================
---- tornado-6.0.4.orig/tornado/iostream.py     2020-03-11 11:42:49.610254636 
+0100
-+++ tornado-6.0.4/tornado/iostream.py  2020-03-11 11:45:31.015164413 +0100
-@@ -1677,5 +1677,7 @@ class PipeIOStream(BaseIOStream):
+--- tornado-6.5.5.orig/tornado/iostream.py
++++ tornado-6.5.5/tornado/iostream.py
+@@ -1613,5 +1613,7 @@ class PipeIOStream(BaseIOStream):
  
  def doctests() -> Any:
      import doctest

++++++ tornado-6.5.4.tar.gz -> tornado-6.5.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/PKG-INFO new/tornado-6.5.5/PKG-INFO
--- old/tornado-6.5.4/PKG-INFO  2025-12-15 19:43:01.912789000 +0100
+++ new/tornado-6.5.5/PKG-INFO  2026-03-10 22:14:52.714759300 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: tornado
-Version: 6.5.4
+Version: 6.5.5
 Summary: Tornado is a Python web framework and asynchronous networking 
library, originally developed at FriendFeed.
 Home-page: http://www.tornadoweb.org/
 Author: Facebook
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/docs/releases/v6.5.5.rst 
new/tornado-6.5.5/docs/releases/v6.5.5.rst
--- old/tornado-6.5.4/docs/releases/v6.5.5.rst  1970-01-01 01:00:00.000000000 
+0100
+++ new/tornado-6.5.5/docs/releases/v6.5.5.rst  2026-03-10 22:14:45.000000000 
+0100
@@ -0,0 +1,19 @@
+What's new in Tornado 6.5.5
+===========================
+
+Mar 10, 2026
+------------
+
+Security fixes
+~~~~~~~~~~~~~~
+
+- ``multipart/form-data`` requests are now limited to 100 parts by default, to 
prevent a
+  denial-of-service attack via very large requests with many parts. This limit 
is configurable
+  via `tornado.httputil.ParseMultipartConfig`. Multipart parsing can also be 
disabled completely
+  if not required for the application. Thanks to 
[0x-Apollyon](https://github.com/0x-Apollyon) and
+  [bekkaze](https://github.com/bekkaze) for reporting this issue.
+- The ``domain``, ``path``, and ``samesite`` arguments to 
`.RequestHandler.set_cookie` are now
+  validated for illegal characters, which could be abused to inject other 
attributes on the cookie.
+  Thanks to Dhiral Vyas (Praetorian) for reporting this issue.
+- Carriage return characters are no longer accepted in ``multipart/form-data`` 
headers. Thanks to 
+  [sergeykochanov](https://github.com/sergeykochanov) for reporting this issue.
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/docs/releases.rst 
new/tornado-6.5.5/docs/releases.rst
--- old/tornado-6.5.4/docs/releases.rst 2025-12-15 19:42:58.000000000 +0100
+++ new/tornado-6.5.5/docs/releases.rst 2026-03-10 22:14:45.000000000 +0100
@@ -4,6 +4,7 @@
 .. toctree::
    :maxdepth: 2
 
+   releases/v6.5.5
    releases/v6.5.4
    releases/v6.5.3
    releases/v6.5.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado/__init__.py 
new/tornado-6.5.5/tornado/__init__.py
--- old/tornado-6.5.4/tornado/__init__.py       2025-12-15 19:42:59.000000000 
+0100
+++ new/tornado-6.5.5/tornado/__init__.py       2026-03-10 22:14:45.000000000 
+0100
@@ -22,8 +22,8 @@
 # is zero for an official release, positive for a development branch,
 # or negative for a release candidate or beta (after the base version
 # number has been incremented)
-version = "6.5.4"
-version_info = (6, 5, 4, 0)
+version = "6.5.5"
+version_info = (6, 5, 5, 0)
 
 import importlib
 import typing
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado/httputil.py 
new/tornado-6.5.5/tornado/httputil.py
--- old/tornado-6.5.4/tornado/httputil.py       2025-12-15 19:42:59.000000000 
+0100
+++ new/tornado-6.5.5/tornado/httputil.py       2026-03-10 22:14:45.000000000 
+0100
@@ -22,6 +22,7 @@
 import calendar
 import collections.abc
 import copy
+import dataclasses
 import datetime
 import email.utils
 from functools import lru_cache
@@ -72,7 +73,7 @@
 
 # Roughly the inverse of RequestHandler._VALID_HEADER_CHARS, but permits
 # chars greater than \xFF (which may appear after decoding utf8).
-_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]")
+_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0A-\x1F\x7F]")
 
 
 class _ABNF:
@@ -913,12 +914,90 @@
     return int(val)
 
 
[email protected]
+class ParseMultipartConfig:
+    """This class configures the parsing of ``multipart/form-data`` request 
bodies.
+
+    Its primary purpose is to place limits on the size and complexity of 
request messages
+    to avoid potential denial-of-service attacks.
+
+    .. versionadded:: 6.5.5
+    """
+
+    enabled: bool = True
+    """Set this to false to disable the parsing of ``multipart/form-data`` 
requests entirely.
+
+    This may be desirable for applications that do not need to handle this 
format, since
+    multipart request have a history of DoS vulnerabilities in Tornado. 
Multipart requests
+    are used primarily for ``<input type="file">`` in HTML forms, or in APIs 
that mimic this
+    format. File uploads that use the HTTP ``PUT`` method generally do not use 
the multipart
+    format.
+    """
+
+    max_parts: int = 100
+    """The maximum number of parts accepted in a multipart request.
+
+    Each ``<input>`` element in an HTML form corresponds to at least one 
"part".
+    """
+
+    max_part_header_size: int = 10 * 1024
+    """The maximum size of the headers for each part of a multipart request.
+
+    The header for a part contains the name of the form field and optionally 
the filename
+    and content type of the uploaded file.
+    """
+
+
[email protected]
+class ParseBodyConfig:
+    """This class configures the parsing of request bodies.
+
+    .. versionadded:: 6.5.5
+    """
+
+    multipart: ParseMultipartConfig = dataclasses.field(
+        default_factory=ParseMultipartConfig
+    )
+    """Configuration for ``multipart/form-data`` request bodies."""
+
+
+_DEFAULT_PARSE_BODY_CONFIG = ParseBodyConfig()
+
+
+def set_parse_body_config(config: ParseBodyConfig) -> None:
+    r"""Sets the **global** default configuration for parsing request bodies.
+
+    This global setting is provided as a stopgap for applications that need to 
raise the limits
+    introduced in Tornado 6.5.5, or who wish to disable the parsing of 
multipart/form-data bodies
+    entirely. Non-global configuration for this functionality will be 
introduced in a future
+    release.
+
+    >>> content_type = "multipart/form-data; boundary=foo"
+    >>> multipart_body = b"--foo--\r\n"
+    >>> parse_body_arguments(content_type, multipart_body, {}, {})
+    >>> multipart_config = ParseMultipartConfig(enabled=False)
+    >>> config = ParseBodyConfig(multipart=multipart_config)
+    >>> set_parse_body_config(config)
+    >>> parse_body_arguments(content_type, multipart_body, {}, {})
+    Traceback (most recent call last):
+        ...
+    tornado.httputil.HTTPInputError: ...: multipart/form-data parsing is 
disabled
+    >>> set_parse_body_config(ParseBodyConfig())  # reset to defaults
+
+    .. versionadded:: 6.5.5
+    """
+    global _DEFAULT_PARSE_BODY_CONFIG
+    _DEFAULT_PARSE_BODY_CONFIG = config
+
+
 def parse_body_arguments(
     content_type: str,
     body: bytes,
     arguments: Dict[str, List[bytes]],
     files: Dict[str, List[HTTPFile]],
     headers: Optional[HTTPHeaders] = None,
+    *,
+    config: Optional[ParseBodyConfig] = None,
 ) -> None:
     """Parses a form request body.
 
@@ -928,6 +1007,8 @@
     and ``files`` parameters are dictionaries that will be updated
     with the parsed contents.
     """
+    if config is None:
+        config = _DEFAULT_PARSE_BODY_CONFIG
     if content_type.startswith("application/x-www-form-urlencoded"):
         if headers and "Content-Encoding" in headers:
             raise HTTPInputError(
@@ -948,10 +1029,15 @@
             )
         try:
             fields = content_type.split(";")
+            if fields[0].strip() != "multipart/form-data":
+                # This catches "Content-Type: multipart/form-dataxyz"
+                raise HTTPInputError("Invalid content type")
             for field in fields:
                 k, sep, v = field.strip().partition("=")
                 if k == "boundary" and v:
-                    parse_multipart_form_data(utf8(v), body, arguments, files)
+                    parse_multipart_form_data(
+                        utf8(v), body, arguments, files, 
config=config.multipart
+                    )
                     break
             else:
                 raise HTTPInputError("multipart boundary not found")
@@ -964,6 +1050,8 @@
     data: bytes,
     arguments: Dict[str, List[bytes]],
     files: Dict[str, List[HTTPFile]],
+    *,
+    config: Optional[ParseMultipartConfig] = None,
 ) -> None:
     """Parses a ``multipart/form-data`` body.
 
@@ -976,6 +1064,10 @@
        Now recognizes non-ASCII filenames in RFC 2231/5987
        (``filename*=``) format.
     """
+    if config is None:
+        config = _DEFAULT_PARSE_BODY_CONFIG.multipart
+    if not config.enabled:
+        raise HTTPInputError("multipart/form-data parsing is disabled")
     # The standard allows for the boundary to be quoted in the header,
     # although it's rare (it happens at least for google app engine
     # xmpp).  I think we're also supposed to handle backslash-escapes
@@ -987,12 +1079,16 @@
     if final_boundary_index == -1:
         raise HTTPInputError("Invalid multipart/form-data: no final boundary 
found")
     parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
+    if len(parts) > config.max_parts:
+        raise HTTPInputError("multipart/form-data has too many parts")
     for part in parts:
         if not part:
             continue
         eoh = part.find(b"\r\n\r\n")
         if eoh == -1:
             raise HTTPInputError("multipart/form-data missing headers")
+        if eoh > config.max_part_header_size:
+            raise HTTPInputError("multipart/form-data part header too large")
         headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"), 
_chars_are_bytes=False)
         disp_header = headers.get("Content-Disposition", "")
         disposition, disp_params = _parse_header(disp_header)
@@ -1200,7 +1296,7 @@
     # type: () -> unittest.TestSuite
     import doctest
 
-    return doctest.DocTestSuite()
+    return doctest.DocTestSuite(optionflags=doctest.ELLIPSIS)
 
 
 _netloc_re = re.compile(r"^(.+):(\d+)$")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado/test/httputil_test.py 
new/tornado-6.5.5/tornado/test/httputil_test.py
--- old/tornado-6.5.4/tornado/test/httputil_test.py     2025-12-15 
19:42:59.000000000 +0100
+++ new/tornado-6.5.5/tornado/test/httputil_test.py     2026-03-10 
22:14:45.000000000 +0100
@@ -9,6 +9,7 @@
     qs_to_qsl,
     HTTPInputError,
     HTTPFile,
+    ParseMultipartConfig,
 )
 from tornado.escape import utf8, native_str
 from tornado.log import gen_log
@@ -135,6 +136,8 @@
             'a";";.txt',
             'a\\"b.txt',
             "a\\b.txt",
+            "a b.txt",
+            "a\tb.txt",
         ]
         for filename in filenames:
             logging.debug("trying filename %r", filename)
@@ -155,6 +158,29 @@
             self.assertEqual(file["filename"], filename)
             self.assertEqual(file["body"], b"Foo")
 
+    def test_invalid_chars(self):
+        filenames = [
+            "a\rb.txt",
+            "a\0b.txt",
+            "a\x08b.txt",
+        ]
+        for filename in filenames:
+            str_data = """\
+--1234
+Content-Disposition: form-data; name="files"; filename="%s"
+
+Foo
+--1234--""" % filename.replace(
+                "\\", "\\\\"
+            ).replace(
+                '"', '\\"'
+            )
+            data = utf8(str_data.replace("\n", "\r\n"))
+            args, files = form_data_args()
+            with self.assertRaises(HTTPInputError) as cm:
+                parse_multipart_form_data(b"1234", data, args, files)
+            self.assertIn("Invalid header value", str(cm.exception))
+
     def test_non_ascii_filename_rfc5987(self):
         data = b"""\
 --1234
@@ -298,10 +324,45 @@
             return time.perf_counter() - start
 
         d1 = f(1_000)
+        # Note that headers larger than this are blocked by the default 
configuration.
         d2 = f(10_000)
         if d2 / d1 > 20:
             self.fail(f"Disposition param parsing is not linear: {d1=} vs 
{d2=}")
 
+    def test_multipart_config(self):
+        boundary = b"1234"
+        body = b"""--1234
+Content-Disposition: form-data; name="files"; filename="ab.txt"
+
+--1234--""".replace(
+            b"\n", b"\r\n"
+        )
+        config = ParseMultipartConfig()
+        args, files = form_data_args()
+        parse_multipart_form_data(boundary, body, args, files, config=config)
+        self.assertEqual(files["files"][0]["filename"], "ab.txt")
+
+        config_no_parts = ParseMultipartConfig(max_parts=0)
+        with self.assertRaises(HTTPInputError) as cm:
+            parse_multipart_form_data(
+                boundary, body, args, files, config=config_no_parts
+            )
+        self.assertIn("too many parts", str(cm.exception))
+
+        config_small_headers = ParseMultipartConfig(max_part_header_size=10)
+        with self.assertRaises(HTTPInputError) as cm:
+            parse_multipart_form_data(
+                boundary, body, args, files, config=config_small_headers
+            )
+        self.assertIn("header too large", str(cm.exception))
+
+        config_disabled = ParseMultipartConfig(enabled=False)
+        with self.assertRaises(HTTPInputError) as cm:
+            parse_multipart_form_data(
+                boundary, body, args, files, config=config_disabled
+            )
+        self.assertIn("multipart/form-data parsing is disabled", 
str(cm.exception))
+
 
 class HTTPHeadersTest(unittest.TestCase):
     def test_multi_line(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado/test/web_test.py 
new/tornado-6.5.5/tornado/test/web_test.py
--- old/tornado-6.5.4/tornado/test/web_test.py  2025-12-15 19:42:59.000000000 
+0100
+++ new/tornado-6.5.5/tornado/test/web_test.py  2026-03-10 22:14:45.000000000 
+0100
@@ -1,3 +1,5 @@
+import http
+
 from tornado.concurrent import Future
 from tornado import gen
 from tornado.escape import (
@@ -292,11 +294,67 @@
                 self.set_cookie("unicode_args", "blah", domain="foo.com", 
path="/foo")
 
         class SetCookieSpecialCharHandler(RequestHandler):
+            # "Special" characters are allowed in cookie values, but trigger 
special quoting.
             def get(self):
                 self.set_cookie("equals", "a=b")
                 self.set_cookie("semicolon", "a;b")
                 self.set_cookie("quote", 'a"b')
 
+        class SetCookieForbiddenCharHandler(RequestHandler):
+            def get(self):
+                # Control characters and semicolons raise errors in cookie 
names and attributes
+                # (but not values, which are tested in 
SetCookieSpecialCharHandler)
+                for char in list(map(chr, range(0x20))) + [chr(0x7F), ";"]:
+                    try:
+                        self.set_cookie("foo" + char, "bar")
+                        self.write(
+                            "Didn't get expected exception for char %r in 
name\n" % char
+                        )
+                    except http.cookies.CookieError as e:
+                        if "Invalid cookie attribute name" not in str(e):
+                            self.write(
+                                "unexpected exception for char %r in name: 
%s\n"
+                                % (char, e)
+                            )
+
+                    try:
+                        self.set_cookie("foo", "bar", domain="example" + char 
+ ".com")
+                        self.write(
+                            "Didn't get expected exception for char %r in 
domain\n"
+                            % char
+                        )
+                    except http.cookies.CookieError as e:
+                        if "Invalid cookie attribute domain" not in str(e):
+                            self.write(
+                                "unexpected exception for char %r in domain: 
%s\n"
+                                % (char, e)
+                            )
+
+                    try:
+                        self.set_cookie("foo", "bar", path="/" + char)
+                        self.write(
+                            "Didn't get expected exception for char %r in 
path\n" % char
+                        )
+                    except http.cookies.CookieError as e:
+                        if "Invalid cookie attribute path" not in str(e):
+                            self.write(
+                                "unexpected exception for char %r in path: 
%s\n"
+                                % (char, e)
+                            )
+
+                    try:
+                        self.set_cookie("foo", "bar", samesite="a" + char)
+                        self.write(
+                            "Didn't get expected exception for char %r in 
samesite\n"
+                            % char
+                        )
+                    except http.cookies.CookieError as e:
+                        if "Invalid cookie attribute samesite" not in str(e):
+                            self.write(
+                                "unexpected exception for char %r in samesite: 
%s\n"
+                                % (char, e)
+                            )
+
         class SetCookieOverwriteHandler(RequestHandler):
             def get(self):
                 self.set_cookie("a", "b", domain="example.com")
@@ -330,6 +388,7 @@
             ("/get", GetCookieHandler),
             ("/set_domain", SetCookieDomainHandler),
             ("/special_char", SetCookieSpecialCharHandler),
+            ("/forbidden_char", SetCookieForbiddenCharHandler),
             ("/set_overwrite", SetCookieOverwriteHandler),
             ("/set_max_age", SetCookieMaxAgeHandler),
             ("/set_expires_days", SetCookieExpiresDaysHandler),
@@ -387,6 +446,12 @@
             response = self.fetch("/get", headers={"Cookie": header})
             self.assertEqual(response.body, utf8(expected))
 
+    def test_set_cookie_forbidden_char(self):
+        response = self.fetch("/forbidden_char")
+        self.assertEqual(response.code, 200)
+        self.maxDiff = 10000
+        self.assertMultiLineEqual(to_unicode(response.body), "")
+
     def test_set_cookie_overwrite(self):
         response = self.fetch("/set_overwrite")
         headers = response.headers.get_list("Set-Cookie")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado/web.py 
new/tornado-6.5.5/tornado/web.py
--- old/tornado-6.5.4/tornado/web.py    2025-12-15 19:42:59.000000000 +0100
+++ new/tornado-6.5.5/tornado/web.py    2026-03-10 22:14:45.000000000 +0100
@@ -693,9 +693,30 @@
         # The cookie library only accepts type str, in both python 2 and 3
         name = escape.native_str(name)
         value = escape.native_str(value)
-        if re.search(r"[\x00-\x20]", name + value):
-            # Don't let us accidentally inject bad stuff
+        if re.search(r"[\x00-\x20]", value):
+            # Legacy check for control characters in cookie values. This check 
is no longer needed
+            # since the cookie library escapes these characters correctly now. 
It will be removed
+            # in the next feature release.
             raise ValueError(f"Invalid cookie {name!r}: {value!r}")
+        for attr_name, attr_value in [
+            ("name", name),
+            ("domain", domain),
+            ("path", path),
+            ("samesite", samesite),
+        ]:
+            # Cookie attributes may not contain control characters or 
semicolons (except when
+            # escaped in the value). A check for control characters was added 
to the http.cookies
+            # library in a Feb 2026 security release; as of March it still 
does not check for
+            # semicolons.
+            #
+            # When a semicolon check is added to the standard library (and the 
release has had time
+            # for adoption), this check may be removed, but be mindful of the 
fact that this may
+            # change the timing of the exception (to the generation of the 
Set-Cookie header in
+            # flush()). We m
+            if attr_value is not None and re.search(r"[\x00-\x20\x3b\x7f]", 
attr_value):
+                raise http.cookies.CookieError(
+                    f"Invalid cookie attribute {attr_name}={attr_value!r} for 
cookie {name!r}"
+                )
         if not hasattr(self, "_new_cookie"):
             self._new_cookie = (
                 http.cookies.SimpleCookie()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado.egg-info/PKG-INFO 
new/tornado-6.5.5/tornado.egg-info/PKG-INFO
--- old/tornado-6.5.4/tornado.egg-info/PKG-INFO 2025-12-15 19:43:01.000000000 
+0100
+++ new/tornado-6.5.5/tornado.egg-info/PKG-INFO 2026-03-10 22:14:52.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: tornado
-Version: 6.5.4
+Version: 6.5.5
 Summary: Tornado is a Python web framework and asynchronous networking 
library, originally developed at FriendFeed.
 Home-page: http://www.tornadoweb.org/
 Author: Facebook
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/tornado-6.5.4/tornado.egg-info/SOURCES.txt 
new/tornado-6.5.5/tornado.egg-info/SOURCES.txt
--- old/tornado-6.5.4/tornado.egg-info/SOURCES.txt      2025-12-15 
19:43:01.000000000 +0100
+++ new/tornado-6.5.5/tornado.egg-info/SOURCES.txt      2026-03-10 
22:14:52.000000000 +0100
@@ -121,6 +121,7 @@
 docs/releases/v6.5.2.rst
 docs/releases/v6.5.3.rst
 docs/releases/v6.5.4.rst
+docs/releases/v6.5.5.rst
 tornado/__init__.py
 tornado/__init__.pyi
 tornado/_locale_data.py

Reply via email to