Open Redirect Vulnerabilities
What This Means
An open redirect vulnerability occurs when your website accepts a user-controlled input (URL parameter, form field, etc.) and redirects users to that location without proper validation. Attackers exploit this by crafting malicious URLs that appear to come from your trusted domain but redirect victims to phishing sites, malware, or scam pages.
How Open Redirects Work
Vulnerable URL Pattern:
https://trusted-site.com/redirect?url=https://evil.com
https://trusted-site.com/login?next=https://phishing-site.com
https://trusted-site.com/goto?dest=//attacker.com
Attack Flow:
- Attacker crafts URL using your domain with malicious redirect
- Sends URL to victim via email, SMS, social media (looks legitimate)
- Victim clicks, trusts your domain name
- Your site redirects to attacker's site without validation
- Victim thinks they're still on your trusted site
- Attacker's site steals credentials, distributes malware, runs scams
Example Vulnerable Code:
// ❌ VULNERABLE - No validation
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.redirect(url); // Redirects anywhere!
});
Attack URL:
https://yourbank.com/redirect?url=https://fake-yourbank-phishing.com/login
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Attacker's phishing site
Impact on Your Business
Security Risks:
- Phishing attacks - Users tricked into entering credentials on fake sites
- Malware distribution - Redirect to sites that download viruses, trojans
- Scams and fraud - Redirect to fake offers, payment scams
- Session hijacking - Redirect with session tokens captured
- OAuth attacks - Redirect authorization callbacks to attacker
Business Impact:
- Brand damage - Your domain used in phishing campaigns
- User trust erosion - Customers lose confidence after being scammed
- Legal liability - Potential lawsuits from affected users
- SEO penalties - Search engines may flag your site for malicious redirects
- Blacklisting - Email/security filters block your legitimate URLs
- Compliance violations - Security audit failures
Real-World Examples:
- Email: "Reset your password: https://trusted.com/reset?next=https://evil.com"
- Social media: "Get discount: https://shop.com/promo?redirect=https://fake-shop.com"
- SMS phishing: "Verify account: https://bank.com/verify?return=https://phishing.com"
How to Diagnose
Method 1: Manual Testing (Primary)
Test common redirect parameters:
Common parameter names to test:
url,redirect,next,return,returnUrl,goto,dest,destinationcontinue,returnTo,redirect_uri,success_url,callback
Test cases:
# Test 1: External redirect
https://your-site.com/redirect?url=https://google.com
# Test 2: Protocol-relative URL
https://your-site.com/redirect?url=//evil.com
# Test 3: JavaScript protocol
https://your-site.com/redirect?url=javascript:alert('XSS')
# Test 4: Data protocol
https://your-site.com/redirect?url=data:text/html,<script>alert('XSS')</script>
# Test 5: @ symbol trick
https://your-site.com/redirect?url=https://your-site.com@evil.com
# Test 6: URL encoding
https://your-site.com/redirect?url=https%3A%2F%2Fevil.com
# Test 7: Backslash trick
https://your-site.com/redirect?url=https://evil.com\\.your-site.com
# Test 8: Open redirect with subdomain
https://your-site.com/redirect?url=https://evil.your-site.com.attacker.com
Vulnerable if:
- Browser redirects to external site
- JavaScript executes
- Phishing site loads
Secure if:
- Redirect blocked or shows error
- Only redirects to your domain
- Validation error displayed
Method 2: Code Review
Search codebase for redirect patterns:
# Search for redirect functions
grep -r "redirect" --include="*.js" --include="*.php" --include="*.py"
# Look for query parameter usage
grep -r "req.query" --include="*.js"
grep -r "$_GET" --include="*.php"
grep -r "request.args" --include="*.py"
# Find header manipulation
grep -r "Location:" --include="*.php"
grep -r "setHeader.*Location" --include="*.js"
Red flags in code:
// ❌ Direct use of user input
res.redirect(req.query.url);
res.redirect(req.params.next);
window.location = params.get('redirect');
// ❌ No validation
const redirectUrl = request.GET['url'];
return redirect(redirectUrl);
// ❌ Weak validation
if (url.startsWith('http')) {
redirect(url); // Still vulnerable!
}
Method 3: Automated Scanning
Burp Suite:
- Install Burp Suite
- Configure browser proxy
- Browse your site
- Run Active Scan
- Review "Open Redirection" findings
OWASP ZAP:
- Install OWASP ZAP
- Configure proxy
- Spider your website
- Run Active Scan
- Check for "External Redirect" alerts
Custom Script:
import requests
def test_open_redirect(base_url, params):
test_urls = [
'https://evil.com',
'//evil.com',
'javascript:alert(1)',
'https://evil.com\\trusteddomain.com',
]
for param in params:
for test_url in test_urls:
url = f"{base_url}?{param}={test_url}"
try:
response = requests.get(url, allow_redirects=False)
if response.status_code in [301, 302, 303, 307, 308]:
location = response.headers.get('Location', '')
if 'evil.com' in location:
print(f"🚨 VULNERABLE: {url}")
print(f" Redirects to: {location}")
except:
pass
# Test
test_open_redirect(
'https://your-site.com/redirect',
['url', 'next', 'redirect', 'return']
)
Method 4: Browser DevTools
- Open DevTools (
F12) - Go to Network tab
- Enter test URL with external redirect
- Look for 301/302 responses
- Check
Locationheader
Vulnerable example:
Status: 302 Found
Location: https://evil.com
Method 5: Security Headers Check
Check for referrer policy that might help:
curl -I https://your-site.com | grep -i referrer-policy
While not a fix for open redirects, Referrer-Policy: no-referrer or strict-origin limits information leakage.
General Fixes
Fix 1: Whitelist Allowed Domains (Recommended)
Only redirect to trusted domains:
// Node.js/Express
app.get('/redirect', (req, res) => {
const requestedUrl = req.query.url;
// Whitelist of allowed domains
const allowedDomains = [
'yoursite.com',
'www.yoursite.com',
'subdomain.yoursite.com',
'partner.com'
];
try {
const url = new URL(requestedUrl, 'https://yoursite.com');
// Check if hostname is in whitelist
if (allowedDomains.includes(url.hostname)) {
res.redirect(url.href);
} else {
res.status(400).send('Invalid redirect URL');
}
} catch (error) {
res.status(400).send('Invalid URL format');
}
});
// PHP
<?php
$requestedUrl = $_GET['url'] ?? '';
$allowedDomains = ['yoursite.com', 'www.yoursite.com', 'partner.com'];
$parsedUrl = parse_url($requestedUrl);
$hostname = $parsedUrl['host'] ?? '';
if (in_array($hostname, $allowedDomains)) {
header("Location: " . $requestedUrl);
exit;
} else {
http_response_code(400);
echo "Invalid redirect URL";
exit;
}
?>
# Python/Flask
from flask import Flask, request, redirect
from urllib.parse import urlparse
app = Flask(__name__)
ALLOWED_DOMAINS = ['yoursite.com', 'www.yoursite.com', 'partner.com']
@app.route('/redirect')
def safe_redirect():
requested_url = request.args.get('url', '')
try:
parsed = urlparse(requested_url)
if parsed.netloc in ALLOWED_DOMAINS:
return redirect(requested_url)
else:
return "Invalid redirect URL", 400
except:
return "Invalid URL format", 400
Fix 2: Relative URLs Only
Only allow paths on your domain:
// Node.js/Express
app.get('/redirect', (req, res) => {
const path = req.query.path;
// Ensure it's a relative path (starts with /)
if (path && path.startsWith('/') && !path.startsWith('//')) {
res.redirect(path);
} else {
res.status(400).send('Invalid redirect path');
}
});
// PHP
<?php
$path = $_GET['path'] ?? '';
// Check if relative path
if ($path && $path[0] === '/' && !str_starts_with($path, '//')) {
header("Location: " . $path);
exit;
} else {
http_response_code(400);
echo "Invalid redirect path";
exit;
}
?>
# Python/Flask
@app.route('/redirect')
def safe_redirect():
path = request.args.get('path', '')
# Ensure relative path
if path and path.startswith('/') and not path.startswith('//'):
return redirect(path)
else:
return "Invalid redirect path", 400
Fix 3: Indirect Redirect with Mapping
Use IDs instead of URLs:
// Node.js/Express
const REDIRECT_MAP = {
'home': '/',
'products': '/products',
'checkout': '/checkout',
'partner': 'https://trusted-partner.com'
};
app.get('/redirect', (req, res) => {
const redirectKey = req.query.to;
const redirectUrl = REDIRECT_MAP[redirectKey];
if (redirectUrl) {
res.redirect(redirectUrl);
} else {
res.status(400).send('Invalid redirect key');
}
});
// Usage: /redirect?to=checkout
// NOT: /redirect?url=https://evil.com
// PHP
<?php
$redirectMap = [
'home' => '/',
'products' => '/products',
'checkout' => '/checkout',
'partner' => 'https://trusted-partner.com'
];
$redirectKey = $_GET['to'] ?? '';
if (isset($redirectMap[$redirectKey])) {
header("Location: " . $redirectMap[$redirectKey]);
exit;
} else {
http_response_code(400);
echo "Invalid redirect key";
exit;
}
?>
Fix 4: URL Validation Function
Comprehensive validation:
// Node.js
function isValidRedirectUrl(url, allowedDomains) {
try {
// Parse URL
const parsed = new URL(url, 'https://yoursite.com');
// Check protocol (only http/https)
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
// Check if domain is allowed
if (!allowedDomains.includes(parsed.hostname)) {
return false;
}
// Ensure no @ symbol (username in URL)
if (url.includes('@')) {
return false;
}
// Ensure no backslashes
if (url.includes('\\')) {
return false;
}
return true;
} catch (error) {
return false;
}
}
// Usage
app.get('/redirect', (req, res) => {
const url = req.query.url;
const allowedDomains = ['yoursite.com', 'www.yoursite.com'];
if (isValidRedirectUrl(url, allowedDomains)) {
res.redirect(url);
} else {
res.status(400).send('Invalid redirect URL');
}
});
Fix 5: Same-Origin Policy
Only redirect within your domain:
// Node.js/Express
app.get('/redirect', (req, res) => {
const requestedUrl = req.query.url;
try {
const url = new URL(requestedUrl, `https://${req.hostname}`);
// Only allow same hostname
if (url.hostname === req.hostname) {
res.redirect(url.href);
} else {
res.status(400).send('External redirects not allowed');
}
} catch (error) {
res.status(400).send('Invalid URL');
}
});
# Python/Flask
from urllib.parse import urlparse
@app.route('/redirect')
def safe_redirect():
requested_url = request.args.get('url', '')
try:
parsed = urlparse(requested_url)
# Allow only same hostname or relative URLs
if not parsed.netloc or parsed.netloc == request.host:
return redirect(requested_url)
else:
return "External redirects not allowed", 400
except:
return "Invalid URL", 400
Fix 6: User Confirmation for External Redirects
Show warning before redirecting:
// Node.js/Express
app.get('/redirect', (req, res) => {
const requestedUrl = req.query.url;
const allowedDomains = ['yoursite.com', 'www.yoursite.com'];
try {
const url = new URL(requestedUrl);
if (allowedDomains.includes(url.hostname)) {
// Safe redirect
res.redirect(url.href);
} else {
// Show confirmation page for external URLs
res.send(`
<!DOCTYPE html>
<html>
<head><title>External Redirect</title></head>
<body>
<h1>You are leaving our site</h1>
<p>You are being redirected to: <strong>${escapeHtml(url.href)}</strong></p>
<p>Are you sure you want to continue?</p>
<a href="${escapeHtml(url.href)}">Yes, continue to external site</a>
<a href="/">No, return to homepage</a>
</body>
</html>
`);
}
} catch (error) {
res.status(400).send('Invalid URL');
}
});
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Fix 7: OAuth and Callback URL Validation
For OAuth redirect_uri validation:
// Node.js/Express OAuth callback
app.get('/oauth/authorize', (req, res) => {
const redirectUri = req.query.redirect_uri;
const clientId = req.query.client_id;
// Fetch registered redirect URIs for this client
const registeredUris = getClientRedirectUris(clientId);
// Exact match required (no partial matches)
if (registeredUris.includes(redirectUri)) {
// Proceed with OAuth flow
const authCode = generateAuthCode();
res.redirect(`${redirectUri}?code=${authCode}`);
} else {
res.status(400).send('Invalid redirect_uri');
}
});
Important: Never use startsWith() or partial matching for OAuth redirects!
Fix 8: Framework-Specific Protection
Django:
# Django settings.py
ALLOWED_REDIRECT_HOSTS = ['yoursite.com', 'www.yoursite.com']
# In view
from django.utils.http import url_has_allowed_host_and_scheme
def safe_redirect(request):
next_url = request.GET.get('next', '/')
if url_has_allowed_host_and_scheme(
next_url,
allowed_hosts=settings.ALLOWED_REDIRECT_HOSTS,
require_https=True
):
return redirect(next_url)
else:
return redirect('/')
Rails:
# Rails controller
def safe_redirect
requested_url = params[:url]
# Only allow relative paths
if requested_url&.start_with?('/') && !requested_url.start_with?('//')
redirect_to requested_url
else
redirect_to root_path, alert: 'Invalid redirect URL'
end
end
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After fixing open redirects:
Test 1: Manual Redirect Tests
# Test external redirect (should fail)
curl -I "https://your-site.com/redirect?url=https://google.com"
# Expected: 400 Bad Request or redirect to safe page
# Test relative path (should work)
curl -I "https://your-site.com/redirect?path=/products"
# Expected: 302 Found, Location: /products
# Test protocol-relative (should fail)
curl -I "https://your-site.com/redirect?url=//evil.com"
# Expected: 400 Bad Request
# Test JavaScript protocol (should fail)
curl -I "https://your-site.com/redirect?url=javascript:alert(1)"
# Expected: 400 Bad Request
Test 2: Automated Security Scan
- Run OWASP ZAP scan
- Check for "External Redirect" vulnerabilities
- Should see no findings
- Review scan report
Test 3: Code Review Checklist
- All redirect parameters validated
- Whitelist of allowed domains implemented
- No direct use of user input in redirects
- OAuth callback URLs validated with exact match
- Relative URLs properly validated
- No
//prefix allowed - No
@symbol in URLs - No backslashes in URLs
- Protocol validation (http/https only)
- Error handling for invalid redirects
Test 4: Browser Testing
- Try legitimate redirects (should work)
- Try external domains (should fail)
- Try various bypass techniques (should all fail)
- Test on different browsers
- Verify user experience for blocked redirects
Common Mistakes
- Partial domain matching -
url.includes('yoursite.com')can matchevil.com/yoursite.com - startsWith() validation -
url.startsWith('yoursite.com')allowsyoursite.com.evil.com - Regex validation errors - Complex regex easy to bypass
- Forgetting protocol-relative URLs -
//evil.combypasseshttp/httpschecks - Not validating OAuth redirects - Exact match required for security
- Allowing data: or javascript: protocols - Execute code instead of redirect
- Not encoding output - XSS in error messages
- Trusting referrer header - Easily spoofed
- Case sensitivity -
yoursite.comvsYourSite.com - Only checking on client-side - Must validate server-side
Open Redirect Checklist
Prevention:
- Whitelist allowed redirect domains
- Use indirect redirects (ID mapping)
- Validate all redirect parameters
- Use relative URLs when possible
- Implement same-origin policy
- Validate OAuth callback URLs (exact match)
- Show confirmation for external redirects
- Block dangerous protocols (javascript:, data:)
Testing:
- Manual testing with various payloads
- Automated security scanning
- Code review for all redirect logic
- Test bypass techniques
- Verify error handling
- Test on all endpoints
Monitoring:
- Log all redirect attempts
- Alert on suspicious patterns
- Monitor for failed redirects
- Review security scan reports
- Track external redirect requests
Common Vulnerable Patterns
❌ Vulnerable Examples
// 1. No validation
res.redirect(req.query.url);
// 2. Weak validation
if (url.includes('yoursite.com')) redirect(url);
// 3. startsWith validation
if (url.startsWith('https://yoursite.com')) redirect(url);
// 4. Regex bypass
if (/^https:\/\/yoursite\.com/.test(url)) redirect(url);
// 5. Client-side only validation
window.location = validateUrl(params.get('url'));
✅ Secure Examples
// 1. Whitelist validation
const allowedDomains = ['yoursite.com'];
const parsed = new URL(url);
if (allowedDomains.includes(parsed.hostname)) redirect(url);
// 2. Relative paths only
if (path.startsWith('/') && !path.startsWith('//')) redirect(path);
// 3. Indirect redirect
const map = {'home': '/', 'shop': '/shop'};
redirect(map[key]);
// 4. Same-origin only
const parsed = new URL(url, baseUrl);
if (parsed.origin === baseUrl) redirect(url);
// 5. Exact match (OAuth)
if (registeredUris.includes(redirectUri)) redirect(redirectUri);
Bypass Techniques to Test
Protocol-relative URLs:
//evil.com
\\evil.com
Username in URL:
https://yoursite.com@evil.com
https://yoursite.com%00@evil.com
Subdomain tricks:
https://yoursite.com.evil.com
https://evil.com/yoursite.com
Path confusion:
https://yoursite.com/../../../evil.com
URL encoding:
https%3A%2F%2Fevil.com
https://evil%2Ecom
Dangerous protocols:
javascript:alert(1)
data:text/html,<script>alert(1)</script>
vbscript:msgbox(1)
file:///etc/passwd