Django Open Redirect
Guide: Examples
and Prevention

stackhawk

StackHawk|October 20, 2021

In this post, we'll cover Django Open Redirects including a thorough overview, how to fix them, and how to prevent them.

A modern web application usually comprises of a number of different components, each of which is responsible for a specific task. Similarly, it may use a combination of third-party services to handle various tasks: authentication, payment processing, and so on. In as much as this facilitates a simpler and more modular approach to the web application, it also makes the app easier to maintain and to extend.

This necessitates the use of redirects to outbound services or internally to other URLs. Redirects in web development refer to taking users and search engines to a different URL from the one requested. Redirects usually refer to external resources or paths. Internal redirects are usually labelled URL forwarding. Open redirect vulnerabilities occur when the destination URL isn't validated against a set of well-defined redirect patterns.

In this post we'll go through examples of Django open redirects, how to fix them, and prevention.

Django Open Redirect Guide: Examples and Prevention - Picture 1 image

Audience and Prerequisites

This post focuses on web developers, both full-stack and back-end developers. Here, we'll go through redirects in Django and aim to keep your web application safe for your users. 

In order to properly understand the code and explanations, you're advised to have the following: 

  • A high-level understanding of python3 (preferably versions 3.7 and above)

  • Working knowledge of the structure of a Django application

  • Overview understanding of status codes

Overview of Page Redirects in Django

Before we get into open redirects, let's do a quick overview of how to handle redirects in the Django web framework. 

Django handles redirects by returning an instance of HttpResponsePermanentRedirect or HttpResponseRedirect subclasses. The easiest way to achieve this is using the redirect() function from the django.shortcuts module.

Redirect Function

The redirect function takes more than just views; it can take a number of arguments. The first argument is the URL we want to redirect to. The second argument is the status code. The default status code is 302. The third argument is the message to be displayed. The default message is "Found." 

We can redirect to models and pass a dictionary of arguments to the redirect function. This is useful if we want to pass a query string to the redirect URL. You can also create a permanent redirect by passing the permanent=True argument to the redirect function. 

Redirect Loop

When writing redirects, it's common to unknowingly cause an infinite loop. To clarify, this is a situation where URLs are calling each other and the server keeps redirecting, causing an error: too many redirects.

Permanent Redirects

As mentioned above, we can additionally specify a redirect to be permanent. Firstly, we need to clarify how browsers interpret permanent redirects. Once a browser receives a permanent redirect response for a URL, it will, consequently, infinitely cache this response and, conversely, load it faster without having to check the server.

In essence this should be a good thing. However, there are drawbacks. In particular, permanent redirects are just that: permanent. With this in mind, if there are any changes in the future to the URLs, it would undoubtedly be difficult to change a permanent redirect. To put it another way, always use temporary redirects.

Open Redirects in Django

In general, the redirect function will take care of your redirect needs. However, for some advanced use cases, you may need to handle user input—for example, if you want to redirect to a page that requires user input.

There are valid reasons for accepting user input like a URL parameter, which is passed to the redirect function. In this case, failure to validate the URL parameter will result in an open redirect. Below we dive into a sample example of an open redirect, solutions, and prevention.

Examples of Open Redirects in Django

Now, let's take a look at examples of open redirects in Django. Currently, the latest version of Django as of the time of writing is 3.2. We'll use that in the code snippets throughout the article unless stated otherwise. 

Accepting Parameters in Redirects

When developing a web application, we often need to pass parameters to the redirect URL. For example, we might want to redirect to a view that displays a specific product. We can do this by passing a dictionary of arguments to the redirect function. Similarly, we can redirect to a view that displays a specific product by passing the product ID to the redirect function. 

Here's an example. Our view will accept the product ID as an argument and display the product. We can do this by passing the product ID to the redirect function, as shown below:

# views.py
from urllib.parse import urlencode
def product_redirect(request):
base_url = "/product/"
url_params = { "product": 1}
encoded_url = base_url + "?" + urlencode(url_params)
encoded_params = urlencode(url_params)
return redirect("{}{}".format(base_url, encoded_params))
def product_view(request):
return HttpResponse(f"<h1> Product 1 </h1>")

The code above imports the urlencode() function from the urllib.parse module and uses it to encode the URL parameters. We can now access the product ID in the URL by accessing the ?product=1 query string. 

We specify the base URL and the encoded URL parameters in the redirect function. We can now access the product ID in the URL by accessing the ?product=1 query string. Lastly, we then proceed to create a view that displays the product. 

Django Open Redirect Guide: Examples and Prevention - Picture 2 image

Unvalidated Redirects

By accepting user input in the redirect URL, we can potentially allow an attacker to redirect to any URL on the web. This refers to an open redirect. 

There definitely are some valid reasons as to why redirects would not be validated. For instance, we can redirect to a view that displays a specific product by passing the product ID to the redirect function, as shown above. Without validation of the redirect URL, an attacker can redirect to any URL on the web. And this could be a malicious website, where they can execute any number of attacks. Let's look at an example. 

Instead of the correct URL redirecting after registration: 

https://myastoreapp.com/register/?next=/user/

The user is redirected to a potentially malicious website: 

https://mystoreapp.com/login/?next=https://mystoreapp.co/user/

Notice the subtle difference in the URL. The user is redirected to a different website. In this situation, most people won't notice the full URL being changed, thus opening the door on phishing attacks.

Open Redirect Fixes in Django

The simplest solution is to not accept user input in the redirect URL. 

Another fix is to validate the redirect URL. We can do this by adding a validate_redirect_url() function to the django.http.request module. This is markedly crucial for not only URLs but any user input you accept from users. This includes forms. 

Django has sane defaults when it comes to validating redirects. For example, the next parameter passes through validation. However, you can integrate StackHawk, a top-tier security tool to find and fix vulnerabilities, including open redirects, in your code quicker.

Prevention of Open Redirects in Django

If you cannot ascertain the safety of a URL, Django provides a handy is_safe_url() function from the django.utils module. This function takes the URL as an argument and returns a Boolean value. If the URL is safe, it returns a True value. If the URL is not safe, it returns as False.

Here are some examples of safe URLs: 

  • This returns True for safe redirects and points to the same host and scheme.

>>> # Import the function first.
>>> from django.utils.http import is_safe_url
>>> is_safe_url('/user/')
True
  • If require_https is set to True, it returns True for safe redirects, points to the same host and scheme, and uses HTTPS.

>>> is_safe_url('https://myastoreapp.com/product/1', require_https=True)
True
  • A URL is considered safe if it references an external URL specified in _allowed_hosts_:.

>>> is_safe_url('https://django.com/user/', allowed_hosts={'myawesomedjangowebapp.com'})
True

Here are some examples of unsafe URLs:

  • URLs that point to a different host return False.

>>> is_safe_url('https://django.com/profile/')
False
  • If require_https is set to True, URLs that use HTTP are not considered safe.

>>> is_safe_url('http://django.com/profile/', require_https=True)
False
Find and Fix Application Security Vulnerabilities with Automated Testing

Summary

We've covered the basics of redirects in Django. The examples were a high-level overview. To learn more, check out the official documentation. Additionally, we covered some of the more advanced features of redirects. Read on to learn more about the potential cons of redirects if not properly implemented. 

Although Django provides a relatively simple and safe way to implement redirects, let's list the cons: 

  • Potential security risk if not properly implemented

  • Potential to cause infinite redirect loop

  • Additional complexity requiring deeper understanding of how redirects work to debug

  • Definite consideration when writing relevant tests

  • Permanent redirects that are difficult to change down the line

We went through what an open redirect is, how to prevent it, and how to fix it. We also explored the built-in functions to make redirects safer. Furthermore, we rounded up the article with various examples of safe and unsafe URLs. You should now be able to understand the basics of redirects in Django. Go on and handle redirects like a pro! If you find this article helpful, please share. 

This post was written by Ken Mwaura. Ken is a backend developer with over 2 years experience writing in depth tutorials on implementing solutions ranging from text solutions to databases, and using APIs to solve real-world problems.


StackHawk  |  October 20, 2021