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, cookies, headers) through your code to the URL of an outbound HTTP request, 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 who controls the target URL can make your server reach internal-only services (admin panels, databases, other microservices behind your firewall) or the cloud metadata endpoint (e.g. http://169.254.169.254/), stealing IAM/instance credentials and pivoting deeper into your network.
Never pass a raw request.args/request.form/request.values/ request.json/request.cookies/request.headers value to requests/urllib/httpx/aiohttp. Validate the destination host against an explicit allow-list (is_allowed_url(...) / validate_host(...) / url_has_allowed_host_and_scheme(...)) before the request, and reject link-local / private / metadata addresses.
VULNERABLE
vulnerable.py
"""Flask views with SSRF: untrusted request data reaches an outbound HTTP request."""import urllib.requestimport flaskimport httpximport requestsfrom flask import Flask, requestapp = Flask(__name__)@app.route("/f1")def fetch_query_get(): # Direct: request.args.get('url') flows straight into requests.get(). # ruleid: auth.py.flow.ssrf return requests.get(request.args.get("url")).text@app.route("/f2")def fetch_json_indirect(): # Indirection: tainted value assigned to a local, then fetched. target = request.json["endpoint"] # ruleid: auth.py.flow.ssrf return httpx.get(target).text@app.route("/f3")def fetch_form_post(): dest = request.form.get("callback") # ruleid: auth.py.flow.ssrf return requests.post(dest, json={"ok": True}).text@app.route("/f4")def fetch_values_urlopen(): # Tainted value into urllib.request.urlopen. # ruleid: auth.py.flow.ssrf return urllib.request.urlopen(request.values["u"]).read()@app.route("/f5")def fetch_header_client(): # Tainted header value into an httpx.Client() session call. url = request.headers.get("X-Upstream") client = httpx.Client() # ruleid: auth.py.flow.ssrf return client.get(url).text@app.route("/f6")def fetch_qualified_request(): # Fully-qualified flask.request source into requests.get sink. # ruleid: auth.py.flow.ssrf return requests.get(flask.request.args.get("to")).text@app.route("/f7")def fetch_cookie_request_method(): # Tainted cookie value into requests.request(method, url). back = request.cookies.get("origin") # ruleid: auth.py.flow.ssrf return requests.request("GET", back).text
SAFE
safe.py
"""Outbound HTTP requests that are NOT SSRF — must produce zero findings."""from urllib.parse import urlparseimport requestsfrom flask import Flask, requestapp = Flask(__name__)ALLOWED_HOSTS = {"api.internal.example.com", "cdn.example.com"}@app.route("/s1")def fetch_constant(): # Constant, attacker-uncontrolled target. return requests.get("https://api.example.com/status").textdef is_allowed_url(url): # Allow-list style host check. host = urlparse(url).netloc return host in ALLOWED_HOSTS@app.route("/s2")def fetch_validated(): # Tainted, but cleared by an is_allowed_url(...) check before use. url = request.args.get("url") if is_allowed_url(url): return requests.get(url).text return "rejected", 400
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.