new file mode 100644
@@ -0,0 +1,155 @@
+From 66587e51009457274cedec28f5fd43000d129e4e Mon Sep 17 00:00:00 2001
+From: Ben Darnell <ben@bendarnell.com>
+Date: Fri, 6 Mar 2026 14:50:25 -0500
+Subject: [PATCH] web: Validate characters in all cookie attributes.
+
+Our previous control character check was missing a check for
+U+007F, and also semicolons, which are only allowed in quoted
+parts of values. This commit checks all attributes and
+updates the set of disallowed characters.
+
+CVE: CVE-2026-35536
+Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/24a2d96ea115f663b223887deb0060f13974c104]
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ tornado/test/web_test.py | 65 ++++++++++++++++++++++++++++++++++++++++
+ tornado/web.py | 27 +++++++++++++++--
+ 2 files changed, 89 insertions(+), 3 deletions(-)
+
+diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
+index 801a80ed..ae39e8fc 100644
+--- a/tornado/test/web_test.py
++++ b/tornado/test/web_test.py
+@@ -1,3 +1,5 @@
++import http
++
+ from tornado.concurrent import Future
+ from tornado import gen
+ from tornado.escape import (
+@@ -291,11 +293,67 @@ class CookieTest(WebTestCase):
+ 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")
+@@ -329,6 +387,7 @@ class CookieTest(WebTestCase):
+ ("/get", GetCookieHandler),
+ ("/set_domain", SetCookieDomainHandler),
+ ("/special_char", SetCookieSpecialCharHandler),
++ ("/forbidden_char", SetCookieForbiddenCharHandler),
+ ("/set_overwrite", SetCookieOverwriteHandler),
+ ("/set_max_age", SetCookieMaxAgeHandler),
+ ("/set_expires_days", SetCookieExpiresDaysHandler),
+@@ -385,6 +444,12 @@ class CookieTest(WebTestCase):
+ 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 --git a/tornado/web.py b/tornado/web.py
+index 8a740504..4b70ea93 100644
+--- a/tornado/web.py
++++ b/tornado/web.py
+@@ -643,9 +643,30 @@ class RequestHandler(object):
+ # 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
+- raise ValueError("Invalid cookie %r: %r" % (name, value))
++ 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()
@@ -11,6 +11,7 @@ SRC_URI[sha256sum] = "92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237
SRC_URI += "file://CVE-2025-47287.patch \
file://CVE-2025-67724.patch \
file://CVE-2025-67726.patch \
+ file://CVE-2026-35536.patch \
"
inherit pypi python_setuptools_build_meta