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 flaskfrom flask import Flask, Response, redirect, requestapp = 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 urlparsefrom flask import Flask, redirect, request, url_forapp = 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.