]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
demos: Fix open redirects 3551/head
authorBen Darnell <ben@bendarnell.com>
Tue, 9 Dec 2025 15:40:18 +0000 (10:40 -0500)
committerBen Darnell <ben@bendarnell.com>
Tue, 9 Dec 2025 15:40:18 +0000 (10:40 -0500)
Several demos had handlers that redirected to a "next" URL
provided as a query parameter without validating it first.

demos/blog/blog.py
demos/blog/templates/base.html
demos/chat/chatdemo.py
demos/facebook/facebook.py

index e6e23f85b4aab0be8cb1b2163595dff3cd62e340..cfa84f942727035fc8e49abdd94025946a6b264a 100755 (executable)
@@ -132,6 +132,14 @@ class BaseHandler(tornado.web.RequestHandler):
     async def any_author_exists(self):
         return bool(await self.query("SELECT * FROM authors LIMIT 1"))
 
+    def redirect_to_next(self):
+        next = self.get_argument("next", "/")
+        if next.startswith("//") or not next.startswith("/"):
+            # Absolute URLs are not allowed because this would be an open redirect
+            # vulnerability (https://cwe.mitre.org/data/definitions/601.html).
+            raise tornado.web.HTTPError(400)
+        self.redirect(next)
+
 
 class HomeHandler(BaseHandler):
     async def get(self):
@@ -243,7 +251,7 @@ class AuthCreateHandler(BaseHandler):
             tornado.escape.to_unicode(hashed_password),
         )
         self.set_signed_cookie("blogdemo_user", str(author.id))
-        self.redirect(self.get_argument("next", "/"))
+        self.redirect_to_next()
 
 
 class AuthLoginHandler(BaseHandler):
@@ -270,7 +278,7 @@ class AuthLoginHandler(BaseHandler):
         )
         if password_equal:
             self.set_signed_cookie("blogdemo_user", str(author.id))
-            self.redirect(self.get_argument("next", "/"))
+            self.redirect_to_next()
         else:
             self.render("login.html", error="incorrect password")
 
@@ -278,7 +286,7 @@ class AuthLoginHandler(BaseHandler):
 class AuthLogoutHandler(BaseHandler):
     def get(self):
         self.clear_cookie("blogdemo_user")
-        self.redirect(self.get_argument("next", "/"))
+        self.redirect_to_next()
 
 
 class EntryModule(tornado.web.UIModule):
index e21f29a3d23f45e621435b3c133c7116651b91dc..0c94e527baec810ba1f3b3851f38db7548d58e67 100644 (file)
@@ -1,27 +1,31 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <meta charset="UTF-8">
-    <title>{{ escape(handler.settings["blog_title"]) }}</title>
-    <link rel="stylesheet" href="{{ static_url("blog.css") }}" type="text/css">
-    <link rel="alternate" href="/feed" type="application/atom+xml" title="{{ escape(handler.settings["blog_title"]) }}">
-    {% block head %}{% end %}
-  </head>
-  <body>
-    <div id="body">
-      <div id="header">
-        <div style="float:right">
-          {% if current_user %}
-            <a href="/compose">{{ _("New post") }}</a> -
-            <a href="/auth/logout?next={{ url_escape(request.uri) }}">{{ _("Sign out") }}</a>
-          {% else %}
-            {% raw _('<a href="%(url)s">Sign in</a> to compose/edit') % {"url": "/auth/login?next=" + url_escape(request.uri)} %}
-          {% end %}
-        </div>
-        <h1><a href="/">{{ escape(handler.settings["blog_title"]) }}</a></h1>
+
+<head>
+  <meta charset="UTF-8">
+  <title>{{ escape(handler.settings["blog_title"]) }}</title>
+  <link rel="stylesheet" href="{{ static_url(" blog.css") }}" type="text/css">
+  <link rel="alternate" href="/feed" type="application/atom+xml" title="{{ escape(handler.settings[" blog_title"]) }}">
+  {% block head %}{% end %}
+</head>
+
+<body>
+  <div id="body">
+    <div id="header">
+      <div style="float:right">
+        {% if current_user %}
+        <a href="/compose">{{ _("New post") }}</a> -
+        <a href="/auth/logout?next={{ url_escape(request.path) }}">{{ _("Sign out") }}</a>
+        {% else %}
+        {% raw _('<a href="%(url)s">Sign in</a> to compose/edit') % {"url": "/auth/login?next=" +
+        url_escape(request.path)} %}
+        {% end %}
       </div>
-      <div id="content">{% block body %}{% end %}</div>
+      <h1><a href="/">{{ escape(handler.settings["blog_title"]) }}</a></h1>
     </div>
-    {% block bottom %}{% end %}
-  </body>
-</html>
+    <div id="content">{% block body %}{% end %}</div>
+  </div>
+  {% block bottom %}{% end %}
+</body>
+
+</html>
\ No newline at end of file
index 8cc6f65aa5f99565ca7612a04c1ed36b13bb921a..9878b3b91f9bf59050f36e48b4f8afa63b406361 100755 (executable)
@@ -71,8 +71,12 @@ class MessageNewHandler(tornado.web.RequestHandler):
         message["html"] = tornado.escape.to_unicode(
             self.render_string("message.html", message=message)
         )
-        if self.get_argument("next", None):
-            self.redirect(self.get_argument("next"))
+        if next := self.get_argument("next", None):
+            if next.startswith("//") or not next.startswith("/"):
+                # Absolute URLs are not allowed because this would be an open redirect
+                # vulnerability (https://cwe.mitre.org/data/definitions/601.html).
+                raise tornado.web.HTTPError(400)
+            self.redirect(next)
         else:
             self.write(message)
         global_message_buffer.add_message(message)
index 9b608aaf0d381e7f4738e9eadbd2afa156aaaf7e..84e87ff161553a0002526c446a5cecce9fe92564 100755 (executable)
@@ -70,13 +70,7 @@ class MainHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
 
 class AuthLoginHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
     async def get(self):
-        my_url = (
-            self.request.protocol
-            + "://"
-            + self.request.host
-            + "/auth/login?next="
-            + tornado.escape.url_escape(self.get_argument("next", "/"))
-        )
+        my_url = self.request.protocol + "://" + self.request.host + "/auth/login"
         if self.get_argument("code", False):
             user = await self.get_authenticated_user(
                 redirect_uri=my_url,
@@ -85,7 +79,7 @@ class AuthLoginHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
                 code=self.get_argument("code"),
             )
             self.set_signed_cookie("fbdemo_user", tornado.escape.json_encode(user))
-            self.redirect(self.get_argument("next", "/"))
+            self.redirect("/")
             return
         self.authorize_redirect(
             redirect_uri=my_url,
@@ -97,7 +91,7 @@ class AuthLoginHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
 class AuthLogoutHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
     def get(self):
         self.clear_cookie("fbdemo_user")
-        self.redirect(self.get_argument("next", "/"))
+        self.redirect("/")
 
 
 class PostModule(tornado.web.UIModule):