Open Redirect Remediation
Risk Severity
๐ก Medium Fix Effort
โก Low (Quick Fix) Est. Time
โฑ๏ธ 1-2 hours Reference
A01:2021 CWE-601
Open redirects allow attackers to redirect users to malicious sites using your trusted domain. While often considered low severity, they enable phishing attacks and can be chained with other vulnerabilities.
Phishing Enabler
Open redirects are a phishing attacker's dream. A link like
trusted-bank.com/redirect?url=evil.com
appears legitimate but redirects to a credential harvesting site.
Understanding Open Redirects
Vulnerable Patterns
URL parameters:
/login?redirect=https://evil.com /go?url=//evil.com Bypass techniques:
//evil.com https://trusted.com@evil.com /\/evil.com Remediation Strategies
1. Allowlist Domains
Only redirect to known, trusted domains.
2. Use Relative Paths
Only allow redirects to paths within your own site.
3. Indirect References
Map IDs to URLs server-side instead of accepting URLs directly.
Python (Flask)
python
from urllib.parse import urlparse, urljoin
from flask import request, redirect, url_for
ALLOWED_HOSTS = {'mysite.com', 'www.mysite.com', 'app.mysite.com'}
def is_safe_url(target):
"""Check if URL is safe for redirect."""
if not target:
return False
# Parse the target URL
parsed = urlparse(target)
# Allow relative URLs (no scheme or netloc)
if not parsed.scheme and not parsed.netloc:
# Ensure it doesn't start with // (protocol-relative)
if target.startswith('//') or target.startswith('\/'):
return False
return True
# For absolute URLs, check against allowlist
if parsed.scheme in ('http', 'https'):
return parsed.netloc in ALLOWED_HOSTS
return False
@app.route('/redirect')
def safe_redirect():
next_url = request.args.get('next', '/')
if not is_safe_url(next_url):
return redirect('/') # Default to home
return redirect(next_url)Node.js (Express)
javascript
const url = require('url');
const ALLOWED_HOSTS = new Set([
'mysite.com',
'www.mysite.com',
'app.mysite.com'
]);
function isSafeUrl(target, req) {
if (!target) return false;
// Check for protocol-relative URLs
if (target.startsWith('//') || target.startsWith('\\/')) {
return false;
}
try {
const reqHost = req.get('host');
const parsed = new URL(target, 'https://' + reqHost);
// If it's a relative URL, the host will be the request host
if (parsed.host === reqHost) {
return true;
}
// Check against allowlist for absolute URLs
return ALLOWED_HOSTS.has(parsed.host);
} catch (e) {
return false;
}
}
app.get('/redirect', (req, res) => {
const nextUrl = req.query.next || '/';
if (!isSafeUrl(nextUrl, req)) {
return res.redirect('/');
}
res.redirect(nextUrl);
});PHP
php
<?php
$ALLOWED_HOSTS = ['mysite.com', 'www.mysite.com', 'app.mysite.com'];
function isSafeUrl($target) {
global $ALLOWED_HOSTS;
if (empty($target)) {
return false;
}
// Block protocol-relative URLs
if (preg_match('/^[\\\/]{2}/', $target)) {
return false;
}
$parsed = parse_url($target);
// Allow relative URLs (no scheme and no host)
if (!isset($parsed['scheme']) && !isset($parsed['host'])) {
return true;
}
// For absolute URLs, validate host
if (isset($parsed['host'])) {
return in_array($parsed['host'], $ALLOWED_HOSTS, true);
}
return false;
}
$next = $_GET['next'] ?? '/';
if (!isSafeUrl($next)) {
header('Location: /');
exit;
}
header('Location: ' . $next);
exit;
?>Alternative: Indirect References
python
# Instead of accepting URLs directly, use IDs
REDIRECT_MAPPING = {
'dashboard': '/dashboard',
'profile': '/user/profile',
'settings': '/user/settings',
'docs': 'https://docs.mysite.com/',
}
@app.route('/go/<redirect_id>')
def indirect_redirect(redirect_id):
target = REDIRECT_MAPPING.get(redirect_id)
if not target:
return redirect('/')
return redirect(target)๐งช Testing Verification
Open Redirect Test Payloads
text
# Basic external redirects - should be blocked
https://evil.com
http://evil.com
//evil.com
# Protocol-relative with variations
\/\/evil.com
/\/evil.com
# URL with credentials (bypass attempt)
https://trusted.com@evil.com
https://evil.com#@trusted.com
# Encoded payloads
https:%2f%2fevil.com
%2f%2fevil.com
# Data/javascript URLs
data:text/html,<script>alert(1)</script>
javascript:alert(1)
# Path confusion
/https://evil.com
/.evil.com
# Expected: All should redirect to safe default or be blockedCommon Mistakes
Simple String Check
python
# DON'T: Easily bypassed
if url.startswith('https://mysite'):
redirect(url)
# Bypassed with: https://mysite.evil.comCorrect Approach
python
# DO: Parse and validate host
parsed = urlparse(url)
if parsed.netloc in ALLOWED_HOSTS:
redirect(url)Forgetting Protocol-Relative
python
# DON'T: Only check http/https
if not url.startswith(('http://', 'https://')):
# Assume it's relative
# Bypassed with: //evil.comCorrect Approach
python
# DO: Block protocol-relative URLs
if url.startswith('//') or url.startswith('\\/'):
reject(url)Open Redirect Prevention Checklist
- โ URL parsed properly before validation
- โ Host/domain validated against allowlist
- โ Protocol-relative URLs blocked (//evil.com)
- โ Relative paths validated (no /\/, etc.)
- โ URL encoding handled correctly
- โ Fallback to safe default for invalid URLs
- โ Consider using indirect references instead