Skip to main content

Overview

An Open Redirect vulnerability occurs when an application uses user-supplied input to redirect the user to a URL, but fails to validate that URL. An attacker can craft a link to your trusted website that, when clicked, redirects the user to a malicious site. This is often used in phishing attacks, as the user only sees your legitimate domain in the link.

Business Impact

This vulnerability is a powerful tool for phishing. Attackers can steal user credentials by redirecting them to a clone of your login page. It abuses the trust users have in your domain, leading to account compromise and reputational damage.

Reference Details

CWE ID: CWE-601 OWASP Top 10 (2021): A01:2021 - Broken Access Control Severity: Medium

Framework-Specific Analysis and Remediation

This vulnerability is common in login pages, logout endpoints, and any “interstitial” page that uses a query parameter (e.g., ?next=, ?returnTo=) to control navigation. The key to mitigation is to never trust this user-supplied URL. It must be validated against an allow-list or checked to ensure it’s a local path.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

A login view that takes a ?next parameter and uses redirect(request.GET.get('next')).

Vulnerable Scenario 1: A Login Redirect

# accounts/views.py
from django.shortcuts import redirect

def login_view(request):
    if request.method == 'POST':
        # ... (login logic) ...
        if user_is_authenticated:
            next_url = request.GET.get('next')
            if next_url:
                # DANGEROUS: If next_url is "[https://evil.com](https://evil.com)",
                # the user will be redirected there.
                return redirect(next_url)
            return redirect('/')

Vulnerable Scenario 2: A “Change Language” Endpoint

A view that sets a language cookie and redirects the user back to where they came from.
# localization/views.py
def set_language(request):
    return_to = request.GET.get('return_to', '/')
    lang_code = request.GET.get('lang', 'en')
    response = redirect(return_to) # DANGEROUS
    response.set_cookie('language', lang_code)
    return response

Mitigation and Best Practices

Use django.utils.http.url_has_allowed_host_and_scheme to validate the next_url. You must pass it the allowed hosts (from settings.ALLOWED_HOSTS). For relative paths, you can also check next_url.startswith('/').

Secure Code Example

# accounts/views.py (Secure)
from django.shortcuts import redirect
from django.utils.http import url_has_allowed_host_and_scheme
from django.conf import settings

def login_view(request):
    if request.method == 'POST':
        # ... (login logic) ...
        if user_is_authenticated:
            next_url = request.GET.get('next')
            
            # SECURE: Check if the URL is local and safe
            if next_url and url_has_allowed_host_and_scheme(
                url=next_url,
                allowed_hosts={request.get_host()},
                require_https=request.is_secure()
            ):
                return redirect(next_url)
            
            # Default to a safe internal URL
            return redirect('/')

Testing Strategy

Write an integration test that logs in and passes a malicious next parameter. Assert that the response redirects to the default page (/), not the malicious URL.
# accounts/tests.py
def test_login_redirect_prevents_open_redirect(self):
    malicious_url = "[https://evil-site.com](https://evil-site.com)"
    response = self.client.post(
        reverse('login') + f'?next={malicious_url}', 
        {'username': 'u', 'password': 'p'}
    )
    
    # Should redirect to the default, not the malicious URL
    self.assertRedirects(response, '/')