# Open Redirect via Unvalidated Redirect Target

Language: Python
Severity: Medium
CWE: CWE-601

## Source
11

## Flow
11-15

## Sink
15

## 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')
    return_path = request.args.get('return_path', '/dashboard')
    if resource_id and bucket_name:
        presigned_url = s3_client.generate_presigned_url('get_object', Params={'Bucket': bucket_name, 'Key': resource_id}, ExpiresIn=3600)
        return redirect(return_path + '?presigned=' + presigned_url)
    return redirect(return_path)
```

## Explanation

The application accepts user-controlled input via the 'return_path' parameter without any validation and directly passes it to the redirect() function. An attacker can supply an external URL (e.g., https://evil.com) to redirect users to malicious sites after legitimate cloud operations, enabling phishing attacks.

## Remediation

The fix adds a validation function `is_safe_redirect_path()` that ensures the `return_path` parameter is a relative path on the same host by rejecting any URL containing a scheme, netloc, or protocol-relative prefix. If validation fails, the redirect 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', '/shared', '/downloads']

def is_safe_redirect_path(target):
    """Validate that the redirect target is a safe relative path."""
    parsed = urlparse(target)
    # Reject any URL with a scheme or netloc (external URLs)
    if parsed.scheme or parsed.netloc:
        return False
    # Reject protocol-relative URLs (e.g., //evil.com)
    if target.startswith('//'):
        return False
    # Ensure the path starts with / (relative to current host)
    if not 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')
    return_path = request.args.get('return_path', '/dashboard')
    
    # Validate return_path to prevent open redirect
    if not is_safe_redirect_path(return_path):
        return_path = '/dashboard'
    
    if resource_id and bucket_name:
        presigned_url = s3_client.generate_presigned_url('get_object', Params={'Bucket': bucket_name, 'Key': resource_id}, ExpiresIn=3600)
        return redirect(return_path + '?presigned=' + presigned_url)
    return redirect(return_path)
```
