From eb61029aa268456890c68ed1565dcaac1fdafb4a Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Thu, 30 Mar 2023 20:03:28 +0000 Subject: [PATCH] web: Support renaming the XSRF cookie This makes it possible to use the __Host- cookie prefix for increased security --- docs/web.rst | 9 ++++++ tornado/test/web_test.py | 59 ++++++++++++++++++++++++++++++++++++++++ tornado/web.py | 6 ++-- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/web.rst b/docs/web.rst index 3d6a7ba18..956336bda 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -253,12 +253,21 @@ * ``xsrf_cookie_kwargs``: May be set to a dictionary of additional arguments to be passed to `.RequestHandler.set_cookie` for the XSRF cookie. + * ``xsrf_cookie_name``: Controls the name used for the XSRF + cookie (default ``_xsrf``). The intended use is to take + advantage of `cookie prefixes`_. Note that cookie prefixes + interact with other cookie flags, so they must be combined + with ``xsrf_cookie_kwargs``, such as + ``{"xsrf_cookie_name": "__Host-xsrf", "xsrf_cookie_kwargs": + {"secure": True}}`` * ``twitter_consumer_key``, ``twitter_consumer_secret``, ``friendfeed_consumer_key``, ``friendfeed_consumer_secret``, ``google_consumer_key``, ``google_consumer_secret``, ``facebook_api_key``, ``facebook_secret``: Used in the `tornado.auth` module to authenticate to various APIs. + .. _cookie prefixes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes + Template settings: * ``autoescape``: Controls automatic escaping for templates. diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index f1760b9a8..4b2545686 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -2919,6 +2919,65 @@ class XSRFTest(SimpleHandlerTestCase): self.assertEqual(response.code, 200) +# A subset of the previous test with a different cookie name +class XSRFCookieNameTest(SimpleHandlerTestCase): + class Handler(RequestHandler): + def get(self): + self.write(self.xsrf_token) + + def post(self): + self.write("ok") + + def get_app_kwargs(self): + return dict( + xsrf_cookies=True, + xsrf_cookie_name="__Host-xsrf", + xsrf_cookie_kwargs={"secure": True}, + ) + + def setUp(self): + super().setUp() + self.xsrf_token = self.get_token() + + def get_token(self, old_token=None): + if old_token is not None: + headers = self.cookie_headers(old_token) + else: + headers = None + response = self.fetch("/", headers=headers) + response.rethrow() + return native_str(response.body) + + def cookie_headers(self, token=None): + if token is None: + token = self.xsrf_token + return {"Cookie": "__Host-xsrf=" + token} + + def test_xsrf_fail_no_token(self): + with ExpectLog(gen_log, ".*'_xsrf' argument missing"): + response = self.fetch("/", method="POST", body=b"") + self.assertEqual(response.code, 403) + + def test_xsrf_fail_body_no_cookie(self): + with ExpectLog(gen_log, ".*XSRF cookie does not match POST"): + response = self.fetch( + "/", + method="POST", + body=urllib.parse.urlencode(dict(_xsrf=self.xsrf_token)), + ) + self.assertEqual(response.code, 403) + + def test_xsrf_success_post_body(self): + response = self.fetch( + "/", + method="POST", + # Note that renaming the cookie doesn't rename the POST param + body=urllib.parse.urlencode(dict(_xsrf=self.xsrf_token)), + headers=self.cookie_headers(), + ) + self.assertEqual(response.code, 200) + + class XSRFCookieKwargsTest(SimpleHandlerTestCase): class Handler(RequestHandler): def get(self): diff --git a/tornado/web.py b/tornado/web.py index 18634d895..1810cd0cd 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -1486,7 +1486,8 @@ class RequestHandler(object): if version is None: if self.current_user and "expires_days" not in cookie_kwargs: cookie_kwargs["expires_days"] = 30 - self.set_cookie("_xsrf", self._xsrf_token, **cookie_kwargs) + cookie_name = self.settings.get("xsrf_cookie_name", "_xsrf") + self.set_cookie(cookie_name, self._xsrf_token, **cookie_kwargs) return self._xsrf_token def _get_raw_xsrf_token(self) -> Tuple[Optional[int], bytes, float]: @@ -1501,7 +1502,8 @@ class RequestHandler(object): for version 1 cookies) """ if not hasattr(self, "_raw_xsrf_token"): - cookie = self.get_cookie("_xsrf") + cookie_name = self.settings.get("xsrf_cookie_name", "_xsrf") + cookie = self.get_cookie(cookie_name) if cookie: version, token, timestamp = self._decode_xsrf_token(cookie) else: -- 2.47.2