# Open Redirect via Unvalidated Flask redirect() Target

Language: Python
Severity: Medium
CWE: CWE-601

## Source
9

## Flow
9-10

## Sink
10

## Vulnerable Code
```python
from flask import Flask, request, redirect
from functools import wraps

app = Flask(__name__)

def track_referral(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        ref_source = request.args.get('ref_url', '/')
        return redirect(ref_source)
    return decorated

@app.route('/affiliate/click')
@track_referral
def process_affiliate_link():
    return "Processing..."
```

## Explanation

The application accepts user-controlled input from the 'ref_url' parameter without validation and directly passes it to Flask's redirect() function. An attacker can manipulate this parameter to redirect users to malicious external sites, enabling phishing attacks or credential harvesting by making the redirect appear to originate from a trusted domain.

## Remediation

The fix introduces an is_safe_url() validation function that checks whether the redirect target is a safe relative path or belongs to an explicitly allowed set of hosts. If the ref_url parameter fails validation (e.g., it points to an external domain or uses a protocol-relative URL), the redirect defaults to '/' instead of following the attacker-controlled URL.

## Secure Code
```python
from flask import Flask, request, redirect
from functools import wraps
from urllib.parse import urlparse

app = Flask(__name__)

ALLOWED_HOSTS = set()

def is_safe_url(url):
    """Validate that the URL is either relative or points to the same host."""
    if not url:
        return False
    parsed = urlparse(url)
    # Allow only relative URLs (no scheme and no netloc)
    if parsed.scheme == '' and parsed.netloc == '':
        # Reject protocol-relative URLs like //evil.com
        if url.startswith('//'):
            return False
        return True
    # If an absolute URL, ensure the host is in the allowed list
    if parsed.scheme in ('http', 'https') and parsed.netloc in ALLOWED_HOSTS:
        return True
    return False

def track_referral(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        ref_source = request.args.get('ref_url', '/')
        if not is_safe_url(ref_source):
            ref_source = '/'
        return redirect(ref_source)
    return decorated

@app.route('/affiliate/click')
@track_referral
def process_affiliate_link():
    return "Processing..."
```
