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