Flask
Flask is a small framework with a large development surface, and that shape decides this whole cluster. Almost nothing here is a setting somebody chose wrongly. It is either a development affordance that reached production, or a control that was never going to be there because Flask does not ship controls.
Two habits follow from that.
Check the invocation before the configuration. More of this cluster is decided by
how the process was started than by what is in config.py — which is why
the production server is a security page rather than a
performance one.
Do not read the absence of a control as the absence of a need. Flask has no CSRF, no rate limiting and no body-size cap out of the box. That is a scope decision by the framework, not a statement about your application.
Check what you are actually running
Section titled “Check what you are actually running”python3 -c "import flask, werkzeug, jinja2; print(flask.__version__, werkzeug.__version__, jinja2.__version__)"pip list 2>/dev/null | grep -iE '^(flask|werkzeug|jinja2|itsdangerous|flask-wtf) 'Flask 3.1.3 on Werkzeug 3.1.8 is current. Two version boundaries matter here.
TRUSTED_HOSTS arrived in 3.1 — set it on anything older and Flask ignores it
silently, because unknown config keys are not rejected. And FLASK_ENV was removed in
2.3, so a deployment still setting it is controlling nothing; FLASK_DEBUG is the
variable that matters now.
The first thing to check
Section titled “The first thing to check”curl -s -o /dev/null -w '%{http_code}\n' https://your-app.example.com/consoleA 200 there — even one asking for a PIN — is an interactive Python console on a public
port. It is the cluster’s threat page, it is the most
severe thing here by a wide margin, and it arrives through an environment variable
rather than through anyone’s decision.
The PIN that guards it is derived from your username, the module path, the machine’s MAC address and its machine ID. Werkzeug’s own source comment describes half those inputs as existing “to make the cookie unique on the computer, not as a security feature”.
One value underneath four pages
Section titled “One value underneath four pages”SECRET_KEY signs the session cookie, and the cookie is signed rather than encrypted —
the payload base64-decodes to plaintext without it. Anyone holding the key can forge any
session.
Four pages in this cluster end at that value: a debugger console reads it in one
expression, a template injection reaches it as {{ config.SECRET_KEY }}, the session
cookie is only as trustworthy as it is, and CSRF tokens are signed with it too. Read
SECRET_KEY before the pages that depend on it.
The checklist
Section titled “The checklist”Severity is the exposure a control closes, not how often it gets recommended. One entry
is rated low and says so — request size limits, where a reverse proxy has usually
capped the body already and the application limit is defence in depth.
What this cluster does not cover
Section titled “What this cluster does not cover”Python itself, the WSGI server’s TLS, the process manager, and the proxy in front of you. That is the server’s half rather than the application’s — see application vs server for the dividing line, and for the three controls that genuinely span both.