{"title":"Open Redirect via Unvalidated Flask redirect() Target","language":"Python","severity":"Medium","cwe":"CWE-601","source_lines":[9],"flow_lines":[9,10],"sink_lines":[10],"vulnerable_code":"from flask import Flask, request, redirect\nfrom functools import wraps\n\napp = Flask(__name__)\n\ndef track_referral(f):\n    @wraps(f)\n    def decorated(*args, **kwargs):\n        ref_source = request.args.get('ref_url', '/')\n        return redirect(ref_source)\n    return decorated\n\n@app.route('/affiliate/click')\n@track_referral\ndef process_affiliate_link():\n    return \"Processing...\"","explanation":"The application accepts user-controlled input from the 'ref_url' parameter without validation and directly passes it to Flask's redirect() function. An attacker can manipulate this parameter to redirect users to malicious external sites, enabling phishing attacks or credential harvesting by making the redirect appear to originate from a trusted domain.","remediation":"The fix introduces an is_safe_url() validation function that checks whether the redirect target is a safe relative path or belongs to an explicitly allowed set of hosts. If the ref_url parameter fails validation (e.g., it points to an external domain or uses a protocol-relative URL), the redirect defaults to '/' instead of following the attacker-controlled URL.","secure_code":"from flask import Flask, request, redirect\nfrom functools import wraps\nfrom urllib.parse import urlparse\n\napp = Flask(__name__)\n\nALLOWED_HOSTS = set()\n\ndef is_safe_url(url):\n    \"\"\"Validate that the URL is either relative or points to the same host.\"\"\"\n    if not url:\n        return False\n    parsed = urlparse(url)\n    # Allow only relative URLs (no scheme and no netloc)\n    if parsed.scheme == '' and parsed.netloc == '':\n        # Reject protocol-relative URLs like //evil.com\n        if url.startswith('//'):\n            return False\n        return True\n    # If an absolute URL, ensure the host is in the allowed list\n    if parsed.scheme in ('http', 'https') and parsed.netloc in ALLOWED_HOSTS:\n        return True\n    return False\n\ndef track_referral(f):\n    @wraps(f)\n    def decorated(*args, **kwargs):\n        ref_source = request.args.get('ref_url', '/')\n        if not is_safe_url(ref_source):\n            ref_source = '/'\n        return redirect(ref_source)\n    return decorated\n\n@app.route('/affiliate/click')\n@track_referral\ndef process_affiliate_link():\n    return \"Processing...\""}