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 blocked

Common Mistakes

Simple String Check

python
# DON'T: Easily bypassed
if url.startswith('https://mysite'):
    redirect(url)
# Bypassed with: https://mysite.evil.com

Correct 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.com

Correct 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