]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
web: Support renaming the XSRF cookie 3244/head
authorBen Darnell <ben@bendarnell.com>
Thu, 30 Mar 2023 20:03:28 +0000 (20:03 +0000)
committerBen Darnell <ben@bendarnell.com>
Thu, 30 Mar 2023 20:25:26 +0000 (20:25 +0000)
This makes it possible to use the __Host- cookie prefix for increased
security

docs/web.rst
tornado/test/web_test.py
tornado/web.py

index 3d6a7ba18ebb28994ed4deb8d9abc471c171220c..956336bda04054206f4bd2a141331e32eaac86cc 100644 (file)
          * ``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.
index f1760b9a8f70826dcd9873198874a323084548dc..4b25456865a37180304bb4cab07d3f6899984644 100644 (file)
@@ -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):
index 18634d8959c666bd4b6913ceb32abc3985bb7043..1810cd0cd14fbb4ea748bf8c713e8eb9c3e229f3 100644 (file)
@@ -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: