v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
HIGH AI PREVALENCE: HIGH auth.py.flow.open-redirect

Untrusted request data flows into a Flask redirect(...) without validation, an open redirect (CWE-601).

CWE-601 OWASP A01:2021 python DATAFLOW

Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.

Dataflow rule. This is a taint-mode rule: it traces untrusted request input (query, body, params) through your code to an HTTP redirect destination, so indirection across multiple lines is caught, not just the direct one-line form. Routing the value through a recognised validation / allow-list sanitizer clears the taint and suppresses the finding. Why dataflow →

Why this matters

An attacker can craft a link to your trusted domain that bounces the victim to an attacker-controlled site, enabling phishing and OAuth redirect/authorization-code abuse (the victim trusts your URL, then lands on the attacker's page).

Never redirect to a raw request.args/request.form/request.values/ request.cookies/request.headers value. Build the destination with url_for(...) (safe by construction), or validate the target against an explicit allow-list / an is_safe_url(...)-style same-host check before redirecting.

VULNERABLE
vulnerable.py
"""Flask views with open redirects: untrusted request data reaches redirect()."""

import flask
from flask import Flask, Response, redirect, request

app = Flask(__name__)


@app.route("/r1")
def redirect_query_get():
    # Direct: request.args.get('next') flows straight into redirect().
    # ruleid: auth.py.flow.open-redirect
    return redirect(request.args.get("next"))


@app.route("/r2")
def redirect_query_subscript():
    # Indirection: tainted value assigned to a local, then redirected.
    dest = request.args["url"]
    # ruleid: auth.py.flow.open-redirect
    return redirect(dest)


@app.route("/r3")
def redirect_form():
    target = request.form.get("redirect_to")
    # ruleid: auth.py.flow.open-redirect
    return redirect(target, code=302)


@app.route("/r4")
def redirect_values():
    # ruleid: auth.py.flow.open-redirect
    return redirect(request.values["return_url"])


@app.route("/r5")
def redirect_cookie():
    # Tainted cookie value reaching the redirect target.
    back = request.cookies.get("last_page")
    # ruleid: auth.py.flow.open-redirect
    return redirect(back)


@app.route("/r6")
def redirect_qualified_request():
    # Fully-qualified flask.request source + flask.redirect sink.
    # ruleid: auth.py.flow.open-redirect
    return flask.redirect(flask.request.args.get("to"))


@app.route("/r7")
def redirect_header_location():
    # Tainted value placed into a Location response header.
    nxt = request.headers.get("X-Forward-To")
    # ruleid: auth.py.flow.open-redirect
    return Response("", status=302, headers={"Location": nxt})
SAFE
safe.py
"""Flask redirects that are NOT open redirects — must produce zero findings."""

from urllib.parse import urlparse

from flask import Flask, redirect, request, url_for

app = Flask(__name__)

ALLOWED = {"/home", "/dashboard", "/settings"}


@app.route("/s1")
def redirect_url_for():
    # Safe by construction: destination built from a known endpoint name.
    return redirect(url_for("dashboard"))


@app.route("/s2")
def redirect_constant():
    # Constant, attacker-uncontrolled target.
    return redirect("/home")


def is_safe_url(target):
    # Same-host allow-list style check.
    ref = urlparse(request.host_url)
    test = urlparse(target)
    return test.scheme in ("http", "https") and ref.netloc == test.netloc


@app.route("/s3")
def redirect_validated():
    # Tainted, but cleared by an is_safe_url(...) check before use.
    target = request.args.get("next")
    if is_safe_url(target):
        return redirect(target)
    return redirect(url_for("home"))


@app.route("/s4")
def redirect_allow_list():
    # Tainted, but only redirected when present in an explicit allow-list,
    # and even then to the validated constant rather than the raw value.
    dest = request.args.get("to")
    if dest in ALLOWED:
        return redirect(url_for("page", name=dest))
    return redirect("/home")

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

# oauthlint-disable-next-line auth.py.flow.open-redirect -- <reason>

References

https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html ↗https://cwe.mitre.org/data/definitions/601.html ↗