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.ssrf

Untrusted request data flows into an outbound HTTP request without validation, a Server-Side Request Forgery (SSRF, CWE-918).

CWE-918 OWASP API7:2023 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, 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.request

import flask
import httpx
import requests
from flask import Flask, request

app = 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 urlparse

import requests
from flask import Flask, request

app = 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").text


def 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.

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

References

https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html ↗https://cwe.mitre.org/data/definitions/918.html ↗