# Open Redirect via Unvalidated Redirect Target

Language: Python
Severity: Medium
CWE: CWE-601

## Source
10

## Flow
10-13

## Sink
13

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

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

@app.route('/cloud/resource-access')
def grant_temporary_access():
    resource_id = request.args.get('resource_id')
    bucket_name = request.args.get('bucket', 'default-storage')
    return_path = request.args.get('return_path', '/dashboard')
    presigned_url = s3_client.generate_presigned_url('get_object', Params={'Bucket': bucket_name, 'Key': resource_id}, ExpiresIn=3600)
    if presigned_url:
        return redirect(return_path)
    return 'Resource not found', 404
```

## Explanation

The application accepts a user-controlled 'return_path' parameter without any validation and passes it directly to the redirect() function. An attacker can manipulate this parameter to redirect users to external malicious sites, enabling phishing attacks or credential theft after legitimate authentication.

## Remediation

The fix adds a validation function `is_safe_redirect_path` that ensures the return_path is a relative path by rejecting any URL containing a scheme, netloc, or protocol-relative prefix (//). If the return_path fails validation, it defaults to the safe '/dashboard' path, preventing attackers from redirecting users 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_RETURN_PATHS = ['/dashboard', '/files', '/settings', '/profile']

def is_safe_redirect_path(target):
    """Validate that the redirect target is a safe relative path."""
    if not target:
        return False
    parsed = urlparse(target)
    # Reject any URL with a scheme or netloc (external redirect)
    if parsed.scheme or parsed.netloc:
        return False
    # Ensure path starts with / (relative to current host)
    if not target.startswith('/'):
        return False
    # Reject protocol-relative URLs (e.g., //evil.com)
    if target.startswith('//'):
        return False
    return True

@app.route('/cloud/resource-access')
def grant_temporary_access():
    resource_id = request.args.get('resource_id')
    bucket_name = request.args.get('bucket', 'default-storage')
    return_path = request.args.get('return_path', '/dashboard')

    if not is_safe_redirect_path(return_path):
        return_path = '/dashboard'

    presigned_url = s3_client.generate_presigned_url('get_object', Params={'Bucket': bucket_name, 'Key': resource_id}, ExpiresIn=3600)
    if presigned_url:
        return redirect(return_path)
    return 'Resource not found', 404
```
