# Open Redirect via Unvalidated Flask Redirect Target

Language: Python
Severity: Medium
CWE: CWE-601

## Source
11

## Flow
11-14

## Sink
14

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

app = Flask(__name__)
s3_client = boto3.client('s3')

@app.route('/cloud/asset/download')
def fetch_cloud_asset():
    asset_id = request.args.get('asset_id')
    bucket_name = request.args.get('bucket', 'default-assets')
    redirect_url = request.args.get('return_to', '/dashboard')
    if asset_id:
        s3_client.head_object(Bucket=bucket_name, Key=asset_id)
    return redirect(redirect_url)
```

## Explanation

The application accepts a user-controlled 'return_to' parameter and passes it directly to Flask's redirect() function without validation. An attacker can manipulate this parameter to redirect users to arbitrary external malicious sites, enabling phishing attacks or credential theft.

## Remediation

The fix introduces a URL validation function that ensures the redirect target is a relative path (no scheme or netloc) and matches a whitelist of allowed path prefixes. If the provided 'return_to' parameter fails validation, the application defaults to redirecting to '/dashboard', preventing open redirect attacks to external malicious sites.

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

app = Flask(__name__)
s3_client = boto3.client('s3')

ALLOWED_PATHS = ['/dashboard', '/assets', '/files', '/home']

def is_safe_redirect_url(target):
    """Validate that the redirect target is a safe, relative URL."""
    if not target:
        return False
    parsed = urlparse(target)
    # Reject any URL with a scheme or netloc (external URLs)
    if parsed.scheme or parsed.netloc:
        return False
    # Ensure the path starts with / and doesn't use protocol-relative URLs
    if not target.startswith('/') or target.startswith('//'):
        return False
    # Optionally whitelist specific path prefixes
    if not any(target.startswith(allowed) for allowed in ALLOWED_PATHS):
        return False
    return True

@app.route('/cloud/asset/download')
def fetch_cloud_asset():
    asset_id = request.args.get('asset_id')
    bucket_name = request.args.get('bucket', 'default-assets')
    redirect_url = request.args.get('return_to', '/dashboard')
    if asset_id:
        s3_client.head_object(Bucket=bucket_name, Key=asset_id)
    if not is_safe_redirect_url(redirect_url):
        redirect_url = '/dashboard'
    return redirect(redirect_url)
```
