CVE-2023-28370 - Open Redirect Vulnerability in Tornado (What You Should Know, How It Works, and How To Fix)

A serious security bug, CVE-2023-28370, was found in the Python web framework Tornado. Versions 6.3.1 and earlier let attackers use a specially crafted link to redirect visitors to a malicious website. This can easily lead to phishing attacks, where users believe they're logging in to a trusted site, but actually hand over passwords to hackers.

In this post, I'll explain how this bug works, show you sample exploit code, and help you patch your Tornado server.

What's an Open Redirect?

An "open redirect" is when a website lets users control where they're sent after clicking a link or logging in. If the site doesn’t properly check the destination, attackers can send victims to phishing pages or fake login forms, making phishing attacks very easy.

Result: Victim can be sent to any site, including phishing and malware sites

For the official CVE entry, see:  
➡️ MITRE CVE-2023-28370  
➡️ GitHub Advisory  
➡️ Tornado 6.3.2 release notes

How Does the Vulnerability Work?

Many Tornado apps use handlers like RequestHandler.get_argument("next") to allow users to be sent to the right page after login. These parameters are typically named next or redirect.

Attackers can make URLs like

https://evil.com/phish" rel="nofollow">https://trusted-site.com/login?next=https://evil.com/phish

If the server just redirects the user without checking if the next value is valid, anyone clicking the link could end up at the attacker's page after logging in.

Here's a snippet showing the risky redirect pattern

import tornado.web

class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        next_url = self.get_argument("next", "/")
        self.render("login.html", next_url=next_url)

    def post(self):
        # Assume user verified, skip for demo
        next_url = self.get_argument("next", "/")
        self.redirect(next_url)   # <-- VULNERABLE LINE

Suppose a user is sent to

https://badguy.com" rel="nofollow">https://mytornadoapp.com/login?next=https://badguy.com

When they log in, they're instantly redirected to https://badguy.com — the attacker's page.

https://phishingsite.com" rel="nofollow">https://mytornadoapp.com/login?next=https://phishingsite.com

They log in on the real site.

3. The server redirects them straight to https://phishingsite.com.

Here's a minimal "playable" PoC based on Tornado 6.3.1

import tornado.ioloop
import tornado.web

class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('''
            <form action="/login" method="post">
                <input name="next" value="{}" type="hidden" />
                <input name="username" /><input name="password" type="password" />
                <input type="submit" />
            </form>
        '''.format(self.get_argument("next", "/")))

    def post(self):
        next_url = self.get_argument("next", "/")
        self.redirect(next_url)  # Open Redirect flaw

def make_app():
    return tornado.web.Application([
        (r"/login", LoginHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Try:

https://evil.com" rel="nofollow">http://localhost:8888/login?next=https://evil.com

After submitting the form, you’ll be sent to https://evil.com.

How to Patch and Mitigate

The official fix was released in Tornado 6.3.2.

Upgrade your project packages

pip install --upgrade tornado

If you can't upgrade right away, add a check before doing the redirect, such as

from urllib.parse import urlparse

def is_safe_url(url, allowed_hosts):
    parsed = urlparse(url)
    return not parsed.netloc or parsed.netloc in allowed_hosts

class LoginHandler(tornado.web.RequestHandler):
    def post(self):
        next_url = self.get_argument("next", "/")
        if not is_safe_url(next_url, allowed_hosts=["mytornadoapp.com"]):
            next_url = "/"
        self.redirect(next_url)

Or, redirect only to local paths, never full URLs

if next_url.startswith("/"):
    self.redirect(next_url)
else:
    self.redirect("/")

More Reading and References

- Original Tornado security advisory
- Common Open Redirect Patterns and Fixes (OWASP)
- Tornado Documentation
- NIST NVD CVE-2023-28370 summary

In Closing

CVE-2023-28370 is a textbook example of why input validation is critical for web apps. If you're building anything in Tornado, be sure to use the latest version and double-check all places where user input can affect redirects!

Timeline

Published on: 05/25/2023 10:15:00 UTC
Last modified on: 06/01/2023 13:04:00 UTC