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 a hardcoded secret, token or credential through your code to the HTTP response body, 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
Anything you return from a Flask view (via jsonify(...), make_response(...), Response(...)) is visible to every caller, so an env value whose name looks like a credential (API_KEY, CLIENT_SECRET, *_TOKEN, *_PASSWORD, a private key, an access key, ...) must never reach it.
Never send a server secret to the client. Return only the data the caller needs; keep credentials server-side. If a value genuinely must be surfaced, redact or mask it first (redact(...) / mask_secret(...)), and prefer exposing only public configuration (names prefixed PUBLIC_ / NEXT_PUBLIC_ / VITE_) to clients.
VULNERABLE
vulnerable.py
"""Flask views that leak a server secret into the HTTP response (CWE-200)."""import osfrom os import environ, getenvimport flaskfrom flask import Flask, jsonify, make_response, Responseapp = Flask(__name__)@app.route("/v1")def leak_api_key_inline(): # Direct: a credential-named env value returned in the JSON body. # ruleid: auth.py.flow.secret-in-response return jsonify(api_key=os.environ["API_KEY"])@app.route("/v2")def leak_client_secret_indirect(): # Indirection: secret assigned to a local, then returned. secret = os.getenv("CLIENT_SECRET") # ruleid: auth.py.flow.secret-in-response return jsonify(secret=secret)@app.route("/v3")def leak_token_in_dict(): # Secret nested inside the JSON payload dict. # ruleid: auth.py.flow.secret-in-response return jsonify({"token": os.environ["ACCESS_TOKEN"]})@app.route("/v4")def leak_password_make_response(): # Secret returned positionally via make_response. # ruleid: auth.py.flow.secret-in-response return make_response(os.environ["DB_PASSWORD"])@app.route("/v5")def leak_private_key_response(): # Secret returned via the Response constructor. pem = os.getenv("PRIVATE_KEY") # ruleid: auth.py.flow.secret-in-response return Response(pem)@app.route("/v6")def leak_qualified_environ(): # Qualified `from os import environ` source into flask.jsonify. # ruleid: auth.py.flow.secret-in-response return flask.jsonify(credential=environ["SERVICE_CREDENTIAL"])@app.route("/v7")def leak_qualified_getenv(): # Qualified `from os import getenv` source into jsonify. access = getenv("AWS_ACCESS_KEY") # ruleid: auth.py.flow.secret-in-response return jsonify(access_key=access)
SAFE
safe.py
"""Responses that do NOT leak a secret — must produce zero findings."""import osfrom os import getenvfrom flask import Flask, jsonify, make_response, Responseapp = Flask(__name__)def redact(value): # Masks all but the first/last character. if not value: return value return value[0] + "***" + value[-1]@app.route("/s1")def public_env_value(): # Public-by-convention name: not a secret, prefixes are excluded. return jsonify(url=os.environ["NEXT_PUBLIC_API_URL"])@app.route("/s2")def public_prefixed_key(): # PUBLIC_ prefix is excluded even though it contains "KEY". return jsonify(api_key=os.environ["PUBLIC_API_KEY"])@app.route("/s3")def non_secret_env_value(): # Operational config, not a credential. return jsonify(port=os.getenv("PORT"))@app.route("/s4")def constant_value(): # A literal constant, nothing from the environment. return jsonify(status="ok")@app.route("/s5")def redacted_secret(): # Secret is masked before it reaches the response — taint cleared. return jsonify(api_key=redact(os.environ["API_KEY"]))@app.route("/s6")def secret_kept_server_side(): # Secret is used to authenticate upstream, never returned to the client. token = getenv("UPSTREAM_API_TOKEN") _ = {"Authorization": f"Bearer {token}"} return make_response("done")@app.route("/s7")def vite_public_config(): # VITE_ prefix is excluded. return Response(os.getenv("VITE_PUBLIC_TOKEN"))
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.