Introduction

Werkzeug's interactive debugger provides a browser-based Python REPL that can execute arbitrary code on the server. When Flask's debug mode is accidentally enabled in production, the debugger is accessible to anyone who triggers an unhandled exception. The PIN protection is meant as a safeguard, but the PIN is derived from predictable values (username, machine ID, Flask app path) and has been shown to be guessable in containerized environments. An attacker with debugger access can read environment variables, execute system commands, and escalate to full server compromise.

Symptoms

Debugger accessible at error pages:

bash
# Visiting any endpoint that raises an error shows:
# "Werkzeug Debugger" interactive console
# With prompt for PIN: "Please enter the PIN shown in the server console"

PIN printed to stdout:

bash
* Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
 * Debugger is active!
 * Debugger PIN: 123-456-789

Security scanner alert:

bash
CRITICAL: Werkzeug debugger detected on port 5000
PIN: 123-456-789 (derived from predictable machine identifiers)

Common Causes

  • FLASK_DEBUG=1 in production: Environment variable set in production deployment
  • app.run(debug=True) deployed: Debug flag not removed before deployment
  • WERKZEUG_DEBUG_PIN not set: Default PIN derived from machine identifiers
  • Docker image with debug layer: Multi-stage build not stripping debug configuration
  • Configuration file not overridden: Same config file used for dev and production
  • Error handler shows debugger: Custom error handler does not suppress debug output

Step-by-Step Fix

Step 1: Disable debug mode in production

```python # wsgi.py - Production entry point import os from myapp import create_app

# Explicitly disable debug mode app = create_app() app.config['DEBUG'] = False

if __name__ == '__main__': # Only use app.run() for development # Production should use gunicorn/uvicorn app.run(host='0.0.0.0', port=5000) ```

Step 2: Environment-based configuration

```python # config.py import os

class Config: DEBUG = False TESTING = False

class DevelopmentConfig(Config): DEBUG = True

class ProductionConfig(Config): DEBUG = False # Disable Werkzeug debugger entirely # This ensures no debugger even if DEBUG is accidentally True PROPAGATE_EXCEPTIONS = False

# Load config based on environment config_map = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'default': ProductionConfig, # Safe default }

env = os.getenv('FLASK_ENV', 'default') app.config.from_object(config_map[env]) ```

Step 3: Docker-safe production setup

```dockerfile # Dockerfile FROM python:3.11-slim

WORKDIR /app

# Install dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt

# Copy application COPY . .

# Ensure debug is off ENV FLASK_ENV=production ENV FLASK_DEBUG=0 # Disable debugger PIN entirely ENV WERKZEUG_DEBUG_PIN=off

# Use production server EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "wsgi:app"] ```

Prevention

  • Never use app.run(debug=True) in production -- always use gunicorn or uwsgi
  • Set FLASK_ENV=production as the default in Dockerfiles and deployment scripts
  • Add a startup check that verifies DEBUG is False in production environments
  • Use WERKZEUG_DEBUG_PIN=off to disable the debugger entirely
  • Add security scanning to CI/CD that checks for debug mode in production configs
  • Use a WSGI server (gunicorn) that does not include the Werkzeug debugger
  • Log all 500 errors in production with a proper error tracking service (Sentry)