Introduction
An exposed Werkzeug interactive debugger in production is a critical security vulnerability -- effectively a remote code execution backdoor. The Werkzeug debugger provides an interactive Python console that executes arbitrary code on your server. When app.run(debug=True) or use_debugger=True is left enabled in production, anyone who triggers an unhandled exception can access the debugger PIN and execute commands with your application's permissions. This has been the root cause of numerous production server compromises, including cryptocurrency mining deployments, data exfiltration, and lateral movement within networks.
Symptoms
Your application displays the interactive debugger page when an error occurs:
``` Traceback (most recent call last): File "/usr/local/lib/python3.11/site-packages/flask/app.py", line 1484, in full_dispatch_request rv = self.dispatch_request() ... ValueError: invalid literal for int() with base 10: 'abc'
Traceback of module: /app/views.py (line 42) /app/services.py (line 128)
The debugger caught an exception in your WSGI application. You can now look at the traceback which led to the error. If you enable JavaScript you can also use additional features such as code execution (if the evalex feature is enabled), automatic pasting of the values and much more. ```
The page includes an interactive Python console icon next to each frame.
Common Causes
- Debug mode enabled in production config:
app.run(debug=True)orFLASK_DEBUG=1in production environment - Missing environment check: Code that sets
debug=Truebased on a missing or default environment variable - **Werkzeug
DebuggedApplicationmiddleware**: Manually wrapping the WSGI app withwerkzeug.debug.DebuggedApplication - Reverse proxy not filtering error responses: Nginx passing through Werkzeug error pages instead of returning 500
- Container image with debug baked in: Docker image built with
ENV FLASK_DEBUG=1not overridden at runtime
Step-by-Step Fix
Step 1: Disable debug mode based on environment
```python import os from flask import Flask
def create_app(): app = Flask(__name__)
# Read environment - default to production-safe debug_mode = os.environ.get("FLASK_ENV", "production") == "development" app.config["DEBUG"] = debug_mode app.config["TESTING"] = False app.config["PROPAGATE_EXCEPTIONS"] = not debug_mode
return app ```
Run with explicit environment:
```bash # Production - debug OFF FLASK_ENV=production gunicorn myapp:create_app()
# Development - debug ON FLASK_ENV=development flask run ```
Step 2: Use a production WSGI server, not `app.run()`
```python # NEVER use this in production: # if __name__ == "__main__": # app.run(debug=True, host="0.0.0.0")
# Use gunicorn instead: # gunicorn -w 4 -b 0.0.0.0:8000 myapp:create_app() ```
Step 3: Configure custom error handlers
```python from flask import render_template
def create_app(): app = Flask(__name__)
@app.errorhandler(500) def internal_error(error): # Log the full traceback for debugging app.logger.error("Internal server error: %s", error, exc_info=True) # Return a generic error page - no stack traces exposed return render_template("500.html"), 500
@app.errorhandler(404) def not_found(error): return render_template("404.html"), 404
return app ```
Step 4: Verify debug mode is disabled in production
Add a startup check:
```python def create_app(): app = Flask(__name__) # ... configuration ...
# Fail loudly if debug is enabled in production if app.debug and os.environ.get("FLASK_ENV") == "production": raise RuntimeError( "CRITICAL: Flask debug mode must not be enabled in production. " "Set FLASK_ENV=development or debug=False." )
return app ```
Step 5: Configure reverse proxy to handle errors
```nginx # nginx.conf server { listen 80; server_name myapp.example.com;
location / { proxy_pass http://127.0.0.1:8000; proxy_intercept_errors on; error_page 500 502 503 504 /500.html; }
location = /500.html { root /var/www/errors; internal; } } ```
Prevention
- Set
FLASK_ENV=productionas the default in your Dockerfile and systemd service files - Add a CI/CD check that scans for
debug=Truein production configuration files - Use
werkzeug.serving.run_simple()only in development -- gunicorn or uvicorn in production - Set up monitoring for requests to
/__debug__or traceback patterns in logs - Use a vulnerability scanner that checks for exposed debuggers as part of security audits
- Add
PROPAGATE_EXCEPTIONS = Falsein production config to prevent Flask from re-raising errors into the debugger