Running Flask Applications
Flask is a WSGI-based Python web framework, which means it is designed to run behind a WSGI server rather than being directly exposed to production traffic.
Flask for Development
During development, you can run your Flask app using the built-in server by executing:
export FLASK_APP=app.py
export FLASK_ENV=development
flask run
The built-in server listens by default on http://127.0.0.1:5000 which provides useful debugging features, including an interactive debugger and automatic reloads on code changes.
This mode is only recommended for local development and testing purposes.
It is not suitable for production environments due to performance and security limitations.
You can also still running flask using app.run() entrypoint, when app.run() is executed, Flask starts an internal WSGI server.
The parameters host and port define where the server listens, and debug=True enables automatic code reloads and the interactive debugger.
For example:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, Flask!"
if __name__ == "__main__":
app.run(debug=True, host="127.0.0.1", port=5000)
Flask for Production Deployments
For production deployments, Flask applications should run behind a proper WSGI server such as Gunicorn or uWSGI. This ensures better performance, scalability, and error handling.
For example, we can run it using gnuicorn as the following:
gunicorn app:app --bind 0.0.0.0:8000 --workers 4
In this example, Gunicorn starts multiple worker processes to handle concurrent requests and binds to port 8000, making it ready for reverse proxies like Nginx.
Why how you run it matters from an offensive security perspective
The development conveniences shown above are exactly what you hope to find left on in production.
debug=True is the big one. It does two things an attacker wants. First, it serves the Werkzeug interactive debugger, and when an unhandled exception fires, that debugger exposes a Python console in the browser that runs code in the application’s process, which is direct remote code execution. Second, flask run with FLASK_ENV=development enables the same debugger. The console is protected only by a PIN that is derived from predictable host data (username, machine ID, module paths), so it can often be reconstructed rather than guessed. The full PIN-derivation and bypass is in Insecure Flask Debug Mode and PIN Bypass.
app.run(debug=True, host="0.0.0.0", port=5000) # debugger RCE, reachable on every interface
host="0.0.0.0" is the second issue. It binds every network interface, so an app you meant to keep on 127.0.0.1 is now answering the public network directly, in front of whatever reverse proxy and authentication were supposed to sit ahead of it. A dev server bound to 0.0.0.0 with debug=True is a pre-authentication RCE exposed to anyone who can route to the host.
The built-in server is also not a production server for non-security reasons (it is single-threaded by default and not hardened), which is why production uses Gunicorn or uWSGI behind a proxy. From an attacker’s standpoint, spotting the Werkzeug dev server in a banner or error page is a strong signal the target is misconfigured and worth a closer look. When you assess a Flask app, confirm whether debug is on and what interface it binds before anything else.
Mitigation
Keep debug=True and the Werkzeug reloader strictly out of anything reachable beyond your own machine, and never let the value be hardcoded to True in deployable code. Drive it from an environment variable that defaults to off, run production behind Gunicorn or uWSGI fronted by a reverse proxy, and bind the application to 127.0.0.1 so only the proxy can reach it rather than exposing it on 0.0.0.0. If you must keep the dev server bound to a local interface during development, that local binding is the boundary that keeps the debugger console off the network.
import os
from flask import Flask
app = Flask(__name__)
if __name__ == "__main__":
debug = os.environ.get("FLASK_DEBUG") == "1" # off unless explicitly set
app.run(debug=debug, host="127.0.0.1", port=5000)