{"title":"Open Redirect via Unvalidated redirect_uri in OAuth2 Callback","language":"Python","severity":"High","cwe":"CWE-601","source_lines":[9],"flow_lines":[9,14],"sink_lines":[14],"vulnerable_code":"from flask import Flask, request, redirect\nimport requests\napp = Flask(__name__)\nCLIENT_ID = 'iot-dashboard-client'\nTOKEN_ENDPOINT = 'https://auth.iotplatform.internal/token'\n@app.route('/oauth/callback')\ndef device_oauth_callback():\n    auth_code = request.args.get('code')\n    next_hop = request.args.get('redirect_after')\n    tok_resp = requests.post(TOKEN_ENDPOINT, data={'client_id': CLIENT_ID, 'code': auth_code, 'grant_type': 'authorization_code'})\n    if tok_resp.status_code != 200:\n        return 'Token exchange failed', 400\n    access_token = tok_resp.json().get('access_token')\n    if not access_token:\n        return 'No token received', 400\n    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":"from flask import Flask, request, redirect, abort\nfrom urllib.parse import urlparse\nimport requests\n\napp = Flask(__name__)\nCLIENT_ID = 'iot-dashboard-client'\nTOKEN_ENDPOINT = 'https://auth.iotplatform.internal/token'\n\nALLOWED_REDIRECT_HOSTS = {\n    'iotdashboard.example.com',\n    'dashboard.iotplatform.internal',\n}\nDEFAULT_REDIRECT = '/dashboard'\n\n\ndef is_safe_redirect_url(target):\n    \"\"\"Validate that the redirect URL is safe (relative path or whitelisted host).\"\"\"\n    if not target:\n        return False\n    parsed = urlparse(target)\n    # Allow relative paths (no scheme and no netloc)\n    if not parsed.scheme and not parsed.netloc:\n        # Block protocol-relative URLs like //evil.com\n        if target.startswith('//'):\n            return False\n        return True\n    # Allow only http/https to whitelisted hosts\n    if parsed.scheme not in ('http', 'https'):\n        return False\n    if parsed.netloc not in ALLOWED_REDIRECT_HOSTS:\n        return False\n    return True\n\n\n@app.route('/oauth/callback')\ndef device_oauth_callback():\n    auth_code = request.args.get('code')\n    next_hop = request.args.get('redirect_after')\n    tok_resp = requests.post(TOKEN_ENDPOINT, data={'client_id': CLIENT_ID, 'code': auth_code, 'grant_type': 'authorization_code'})\n    if tok_resp.status_code != 200:\n        return 'Token exchange failed', 400\n    access_token = tok_resp.json().get('access_token')\n    if not access_token:\n        return 'No token received', 400\n    if not is_safe_redirect_url(next_hop):\n        next_hop = DEFAULT_REDIRECT\n    return redirect(next_hop)"}