Open Redirect Vulnerabilities | Blue Frog Docs

Open Redirect Vulnerabilities

Detect and fix unvalidated redirect vulnerabilities that attackers exploit for phishing and malware distribution

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:

  1. Attacker crafts URL using your domain with malicious redirect
  2. Sends URL to victim via email, SMS, social media (looks legitimate)
  3. Victim clicks, trusts your domain name
  4. Your site redirects to attacker's site without validation
  5. Victim thinks they're still on your trusted site
  6. 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:

How to Diagnose

Method 1: Manual Testing (Primary)

Test common redirect parameters:

Common parameter names to test:

  • url, redirect, next, return, returnUrl, goto, dest, destination
  • continue, 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:

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:

  1. Install Burp Suite
  2. Configure browser proxy
  3. Browse your site
  4. Run Active Scan
  5. Review "Open Redirection" findings

OWASP ZAP:

  1. Install OWASP ZAP
  2. Configure proxy
  3. Spider your website
  4. Run Active Scan
  5. 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

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Enter test URL with external redirect
  4. Look for 301/302 responses
  5. Check Location header

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

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, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

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:

Platform Troubleshooting Guide
Shopify Shopify Open Redirect Guide
WordPress WordPress Open Redirect Guide
Wix Wix Open Redirect Guide
Squarespace Squarespace Open Redirect Guide
Webflow Webflow Open Redirect Guide

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

  1. Run OWASP ZAP scan
  2. Check for "External Redirect" vulnerabilities
  3. Should see no findings
  4. 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

  1. Try legitimate redirects (should work)
  2. Try external domains (should fail)
  3. Try various bypass techniques (should all fail)
  4. Test on different browsers
  5. Verify user experience for blocked redirects

Common Mistakes

  1. Partial domain matching - url.includes('yoursite.com') can match evil.com/yoursite.com
  2. startsWith() validation - url.startsWith('yoursite.com') allows yoursite.com.evil.com
  3. Regex validation errors - Complex regex easy to bypass
  4. Forgetting protocol-relative URLs - //evil.com bypasses http/https checks
  5. Not validating OAuth redirects - Exact match required for security
  6. Allowing data: or javascript: protocols - Execute code instead of redirect
  7. Not encoding output - XSS in error messages
  8. Trusting referrer header - Easily spoofed
  9. Case sensitivity - yoursite.com vs YourSite.com
  10. 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

Additional Resources

// SYS.FOOTER