# Open Redirect via Unvalidated redirect_uri in OAuth2 Callback

Language: Python
Severity: High
CWE: CWE-601

## Source
9

## Flow
9-14

## Sink
14

## Vulnerable Code
```python
from flask import Flask, request, redirect
import requests
app = Flask(__name__)
CLIENT_ID = 'iot-dashboard-client'
TOKEN_ENDPOINT = 'https://auth.iotplatform.internal/token'
@app.route('/oauth/callback')
def device_oauth_callback():
    auth_code = request.args.get('code')
    next_hop = request.args.get('redirect_after')
    tok_resp = requests.post(TOKEN_ENDPOINT, data={'client_id': CLIENT_ID, 'code': auth_code, 'grant_type': 'authorization_code'})
    if tok_resp.status_code != 200:
        return 'Token exchange failed', 400
    access_token = tok_resp.json().get('access_token')
    if not access_token:
        return 'No token received', 400
    return redirect(next_hop)
```

## Explanation

The 'redirect_after' parameter is read directly from user-controlled query string input on line 9 and passed without any validation or sanitization to Flask's redirect() function on line 14. There is no whitelist check, domain validation, or URL scheme enforcement, allowing an attacker to supply an arbitrary external URL such as 'https://evil.com' as the redirect destination after a successful OAuth2 token exchange.

## Remediation

The fix introduces a URL validation function is_safe_redirect_url() that checks the redirect_after parameter against a whitelist of allowed hosts before passing it to redirect(). Only relative paths (excluding protocol-relative URLs like //evil.com) and absolute URLs with http/https schemes pointing to explicitly whitelisted domains are permitted; all other values fall back to a safe default redirect path (/dashboard).

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

app = Flask(__name__)
CLIENT_ID = 'iot-dashboard-client'
TOKEN_ENDPOINT = 'https://auth.iotplatform.internal/token'

ALLOWED_REDIRECT_HOSTS = {
    'iotdashboard.example.com',
    'dashboard.iotplatform.internal',
}
DEFAULT_REDIRECT = '/dashboard'


def is_safe_redirect_url(target):
    """Validate that the redirect URL is safe (relative path or whitelisted host)."""
    if not target:
        return False
    parsed = urlparse(target)
    # Allow relative paths (no scheme and no netloc)
    if not parsed.scheme and not parsed.netloc:
        # Block protocol-relative URLs like //evil.com
        if target.startswith('//'):
            return False
        return True
    # Allow only http/https to whitelisted hosts
    if parsed.scheme not in ('http', 'https'):
        return False
    if parsed.netloc not in ALLOWED_REDIRECT_HOSTS:
        return False
    return True


@app.route('/oauth/callback')
def device_oauth_callback():
    auth_code = request.args.get('code')
    next_hop = request.args.get('redirect_after')
    tok_resp = requests.post(TOKEN_ENDPOINT, data={'client_id': CLIENT_ID, 'code': auth_code, 'grant_type': 'authorization_code'})
    if tok_resp.status_code != 200:
        return 'Token exchange failed', 400
    access_token = tok_resp.json().get('access_token')
    if not access_token:
        return 'No token received', 400
    if not is_safe_redirect_url(next_hop):
        next_hop = DEFAULT_REDIRECT
    return redirect(next_hop)
```
