Description
An open redirect occurs when an application uses unvalidated user input to redirect to an external URL. Attackers may exploit it for phishing or to direct users to malicious sites.
Potential impact
- Phishing: An attacker may trick users into visiting a malicious site and disclosing sensitive information.
- Loss of trust: Redirects to malicious sites may undermine trust in the application.
- Policy bypass: An attacker may use a redirect to bypass security policies.
Remediation
- Restrict redirect destinations to an allow-list.
- Validate input and use only trusted values for redirects.
- If only internal navigation is required, use server-defined or validated internal paths. Do not treat a path beginning with
//and an external host as an internal path.
Examples
Set allowed hosts for the actual service. Django's home and Flask's index refer to internal routes defined by the application.
Django
Before
python
# Unsafe Django code
from django.http import HttpResponseRedirect
def unsafe_redirect(request):
url = request.GET.get('next')
return HttpResponseRedirect(url)
After
python
# Safe Django code
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.utils.http import url_has_allowed_host_and_scheme
def safe_redirect(request):
url = request.GET.get('next', '')
allowed_hosts = {'example.com', 'mysite.com'}
if url_has_allowed_host_and_scheme(
url,
allowed_hosts=allowed_hosts,
require_https=True,
):
return HttpResponseRedirect(url)
return redirect('home')
Explanation:
- Before: Redirects using unvalidated input, allowing open redirects.
- After: Django's URL validator permits safe relative internal paths or HTTPS URLs with exact allow-listed hostnames. A domain such as
https://example.com.evildoes not pass merely because it begins with an allowed hostname.
Flask
Before
python
# Unsafe Flask code
from flask import Flask, request, redirect
app = Flask(__name__)
@app.route('/redirect')
def unsafe_redirect():
url = request.args.get('next')
return redirect(url)
After
python
# Safe Flask code
from flask import Flask, request, redirect, url_for
from urllib.parse import urlsplit
app = Flask(__name__)
ALLOWED_REDIRECT_HOSTS = frozenset({'example.com', 'mysite.com'})
def is_allowed_redirect(url):
if not url:
return False
try:
parsed = urlsplit(url)
port = parsed.port
except ValueError:
return False
return (
parsed.scheme == 'https'
and parsed.hostname in ALLOWED_REDIRECT_HOSTS
and parsed.username is None
and parsed.password is None
and port in (None, 443)
)
@app.route('/redirect')
def safe_redirect():
url = request.args.get('next')
if is_allowed_redirect(url):
return redirect(url)
return redirect(url_for('index'))
Explanation:
- Before: Redirects using unvalidated input, allowing open redirects.
- After: Parses the URL and checks HTTPS, the exact hostname, the default HTTPS port and the absence of user information. Failure redirects to an internal URL generated by the server.
FastAPI
Before
python
# Unsafe FastAPI code
from fastapi import FastAPI, Request
from starlette.responses import RedirectResponse
app = FastAPI()
@app.get("/redirect")
async def unsafe_redirect(request: Request):
url = request.query_params.get('next')
return RedirectResponse(url)
After
python
# Safe FastAPI code
from fastapi import FastAPI, Request, HTTPException
from starlette.responses import RedirectResponse
from urllib.parse import urlsplit
app = FastAPI()
ALLOWED_REDIRECT_HOSTS = frozenset({'example.com', 'mysite.com'})
def is_allowed_redirect(url):
if not url:
return False
try:
parsed = urlsplit(url)
port = parsed.port
except ValueError:
return False
return (
parsed.scheme == 'https'
and parsed.hostname in ALLOWED_REDIRECT_HOSTS
and parsed.username is None
and parsed.password is None
and port in (None, 443)
)
@app.get("/redirect")
async def safe_redirect(request: Request):
url = request.query_params.get('next')
if is_allowed_redirect(url):
return RedirectResponse(url)
raise HTTPException(status_code=400, detail="Invalid redirect URL")
Explanation:
- Before: Redirects using unvalidated input, allowing open redirects.
- After: Validates URL components to allow only HTTPS and exact permitted hosts, rejecting URLs with credentials or nonstandard ports.