# PYTHON-FLASK-SEC-012: Flask Open Redirect

> **Severity:** MEDIUM | **CWE:** CWE-601 | **OWASP:** A01:2021

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-012
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-SEC-012 --project .`

## Description

This rule detects open redirect vulnerabilities in Flask applications where
user-controlled input from HTTP request parameters flows to Flask's redirect()
function without URL validation. An open redirect occurs when the application
accepts a URL or path from user input and issues an HTTP redirect (302 or 301)
to that destination without verifying it belongs to a trusted origin.

Open redirects are endemic in Flask login flows: after authentication, the
application redirects to the "next" URL parameter. Without validation, an
attacker sends victims to a URL like:
  https://legitimate-app.com/login?next=https://evil.com/phishing

The victim sees the legitimate application's login page, authenticates, and is
transparently redirected to the attacker's site -- all appearing as a natural
post-login navigation. This is a primary vector for credential phishing because
the legitimate domain is visible in the initial URL shared with the victim.

The taint analysis traces the URL value from Flask request sources through variable
assignments and function calls to the Flask redirect() function at argument position 0.
Flows through url_for() or url_has_allowed_host_and_scheme() are recognized as
sanitizers: url_for() generates only application-internal URLs, and
url_has_allowed_host_and_scheme() (from Werkzeug) validates that a URL's host and
scheme are acceptable.


## Vulnerable Code

```python
from flask import Flask, request, redirect

app = Flask(__name__)

@app.route('/goto')
def goto():
    url = request.args.get('url')
    return redirect(url)

@app.route('/redir')
def redir():
    next_page = request.form.get('next')
    return redirect(next_page)
```

## Secure Code

```python
from flask import Flask, request, redirect, url_for, abort
from werkzeug.security import safe_join
from urllib.parse import urlparse, urljoin

app = Flask(__name__)

def is_safe_redirect_url(target):
    """
    Verify the redirect target is relative or points to the same host.
    werkzeug.utils.url_has_allowed_host_and_scheme() does this more robustly.
    """
    host_url = urlparse(request.host_url)
    redirect_url = urlparse(urljoin(request.host_url, target))
    return (
        redirect_url.scheme in ('http', 'https') and
        redirect_url.netloc == host_url.netloc
    )

@app.route('/login', methods=['POST'])
def login():
    # ... authenticate user ...
    next_url = request.args.get('next', '')
    # SAFE: validate before redirecting
    if next_url and is_safe_redirect_url(next_url):
        return redirect(next_url)
    # SAFE fallback: url_for() generates only application-internal URLs
    return redirect(url_for('dashboard'))

@app.route('/logout')
def logout():
    # SAFE: always redirect to a known application route, never user-supplied URL
    return redirect(url_for('index'))

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class FlaskModule(QueryType):
    fqns = ["flask"]


@python_rule(
    id="PYTHON-FLASK-SEC-012",
    name="Flask Open Redirect",
    severity="MEDIUM",
    category="flask",
    cwe="CWE-601",
    tags="python,flask,open-redirect,OWASP-A01,CWE-601",
    message="User input flows to redirect(). Validate redirect URLs against an allowlist.",
    owasp="A01:2021",
)
def detect_flask_open_redirect():
    """Detects Flask request data flowing to redirect()."""
    return flows(
        from_sources=[
            calls("request.args.get"),
            calls("request.form.get"),
            calls("request.values.get"),
            calls("request.get_json"),
        ],
        to_sinks=[
            FlaskModule.method("redirect").tracks(0),
            calls("redirect"),
        ],
        sanitized_by=[
            calls("url_for"),
            calls("url_has_allowed_host_and_scheme"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- For post-login and post-action redirects, use url_for() with a route name rather than accepting a redirect URL from the request -- url_for() can only generate URLs for routes defined in the application.
- If you must accept a redirect URL from user input, validate it with werkzeug's url_has_allowed_host_and_scheme() which checks that the URL's host matches the application's configured allowed hosts.
- Maintain an explicit allowlist of external domains that the application is permitted to redirect to, and reject any redirect URL whose host is not on the list.
- Implement a redirect disclaimer page for redirects to external sites -- show users where they are going and require confirmation before navigating away from the application.
- Log all redirects to external domains for anomaly detection -- legitimate use rarely generates high volumes of redirects to diverse external domains.

## Security Implications

- **Phishing Attack Amplification via Trusted Domain:** Attackers craft URLs like https://yourapp.com/login?next=https://phishing.com/steal
and distribute them. Victims who click the link land on the real application login
page, authenticate normally, and are redirected to the phishing site. Because the
initial URL shows the legitimate domain, anti-phishing training does not help.
The application's credibility is weaponized against its own users.

- **OAuth Token Theft via Redirect URI Manipulation:** In OAuth flows, a vulnerable redirect_uri parameter in the authorization request
can redirect access tokens to an attacker-controlled site. If the Flask application
uses the "next" parameter in OAuth callbacks without validation, the access token
or authorization code in the redirect URL is sent to the attacker's server.

- **SSRF Gadget in Redirect Chains:** Internal services that trust redirects from the application server may follow a
redirect to an internal endpoint. In microservice architectures, an open redirect
in a Flask service can be chained with redirect-following HTTP clients in other
services to reach internal APIs that the attacker cannot access directly.

- **Session Fixation via External Redirect with Cookie Setting:** An attacker who controls the redirect destination can combine the open redirect with
a response that sets a cookie or manipulates browser storage, then redirects back to
the application with a known session token (session fixation). The redirect through
a legitimate domain gives the attacker's cookies the opportunity to be stored.


## FAQ

**Q: Is open redirect really a security vulnerability?**

Yes, though it is lower severity than injection or authentication bypass. Open redirect
is primarily a phishing amplifier -- it allows attackers to use the legitimate
application's domain in URLs that lead to malicious sites. Its real-world impact
is measured by the credibility of the domain and the size of the user base. High-
trust applications (banking, healthcare, government) have significantly higher
phishing risk from open redirects.


**Q: Why is url_for() a sanitizer for this rule?**

url_for() takes a Flask endpoint name (e.g., 'dashboard', 'user.profile') and
generates the URL for that route. It can only produce URLs that correspond to
routes registered in the application. It cannot generate external URLs like
https://evil.com. Using url_for() as the redirect target guarantees the destination
is an internal application route.


**Q: How does url_has_allowed_host_and_scheme() work?**

url_has_allowed_host_and_scheme(url, allowed_hosts) parses the URL and checks
that the hostname is in the allowed_hosts set and the scheme is http or https.
For relative URLs (no hostname), it returns True. Pass request.host as the
allowed host to permit redirects to the same application domain only.


**Q: What about redirects to HTTPS from HTTP? Does that trigger the rule?**

The rule tracks whether the redirect destination value is tainted from user input.
A hardcoded redirect like redirect('https://app.example.com/dashboard') has no
taint and will not trigger. Only redirects where the URL or a part of it traces
back to a Flask request source are flagged.


**Q: We use a "next" parameter in our login form that is validated with a starts-with check on our domain. Is that safe?**

A starts-with check on a string like 'https://yourapp.com' can be bypassed with
URLs like 'https://yourapp.com.evil.com' (if not checking the full hostname) or
'https://yourapp.com@evil.com' (attacker.com becomes the actual host with yourapp.com
as the userinfo). Use url_has_allowed_host_and_scheme() which parses the URL properly
and compares the actual netloc (hostname:port) component.


**Q: How do I handle deep links after OAuth callback where the user should be redirected to their original destination?**

Store the intended destination in the server-side session before the OAuth flow
begins: session['next'] = request.args.get('next'). After OAuth completion,
retrieve it from the session and validate it with url_has_allowed_host_and_scheme()
before redirecting. Never pass the "next" URL through the OAuth provider --
it becomes an open redirect in the OAuth callback.


**Q: Is this rule prone to false positives on redirect(url_for('route')) patterns?**

No. url_for() is recognized as a sanitizer. redirect(url_for('dashboard')) does
not trigger the rule because the URL is generated by url_for(), not from a tainted
source. The rule only fires when the argument to redirect() traces back to a
Flask request parameter without passing through a recognized sanitizer.


## References

- [CWE-601: Open Redirect](https://cwe.mitre.org/data/definitions/601.html)
- [OWASP Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
- [Flask redirect() documentation](https://flask.palletsprojects.com/en/latest/api/#flask.redirect)
- [Werkzeug url_has_allowed_host_and_scheme](https://werkzeug.palletsprojects.com/en/latest/utils/#werkzeug.utils.url_has_allowed_host_and_scheme)
- [Flask url_for() documentation](https://flask.palletsprojects.com/en/latest/api/#flask.url_for)
- [OWASP Testing for Open Redirect](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/04-Testing_for_Client-side_URL_Redirect)

---

Source: https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-012
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
