Flask Debug Mode Enabled in Production

High Risk Information Disclosure
flaskpythondebug-modeinformation-disclosureconfigurationproduction

What it is

The Flask application has debug mode enabled, which exposes sensitive information and creates security vulnerabilities in production environments. Debug mode provides detailed error messages, interactive debugger access, automatic code reloading, and stack traces that can reveal internal application structure, file paths, source code, and configuration details to attackers, potentially facilitating further attacks.

# Vulnerable: Debug mode enabled in Flask

from flask import Flask, request, jsonify
import os
import traceback

# Dangerous: Debug mode explicitly enabled
app = Flask(__name__)
app.debug = True  # CRITICAL: Debug mode in production

# Another dangerous pattern
app.config['DEBUG'] = True

# Environment variable approach (still dangerous if set)
app.config['DEBUG'] = os.environ.get('FLASK_DEBUG', True)

@app.route('/api/data')
def get_data():
    try:
        # Some application logic
        data = process_user_data(request.args)
        return jsonify(data)
    except Exception as e:
        # Dangerous: Detailed error in debug mode
        return jsonify({'error': str(e)}), 500

@app.route('/config')
def show_config():
    # Dangerous: Configuration exposed in debug mode
    return jsonify(dict(app.config))

# Dangerous: Manual debug info exposure
@app.route('/debug/info')
def debug_info():
    if app.debug:
        import sys
        return jsonify({
            'python_version': sys.version,
            'python_path': sys.path,
            'environment': dict(os.environ),
            'config': dict(app.config)
        })
    return jsonify({'error': 'Debug not available'})

# Dangerous: Stack trace exposure
@app.errorhandler(500)
def handle_error(error):
    if app.debug:
        return jsonify({
            'error': str(error),
            'traceback': traceback.format_exc()
        }), 500
    return jsonify({'error': 'Internal server error'}), 500

# Running with debug mode
if __name__ == '__main__':
    # Dangerous: Debug mode in run command
    app.run(debug=True, host='0.0.0.0', port=5000)

# Configuration class approach (vulnerable)
class Config:
    DEBUG = True  # Dangerous: Always debug
    SECRET_KEY = 'dev-secret-key'
    DATABASE_URL = 'sqlite:///dev.db'

class ProductionConfig(Config):
    # Still inherits DEBUG = True - dangerous!
    SECRET_KEY = os.environ.get('SECRET_KEY')
    DATABASE_URL = os.environ.get('DATABASE_URL')

app.config.from_object(ProductionConfig)

# Conditional debug (still dangerous)
def create_app(config_name='development'):
    app = Flask(__name__)
    
    if config_name == 'development':
        app.debug = True
    elif config_name == 'production':
        # Dangerous: Debug might still be enabled elsewhere
        pass
    
    return app

# Environment-based but unsafe default
DEBUG_MODE = os.environ.get('DEBUG', 'True').lower() == 'true'
app.config['DEBUG'] = DEBUG_MODE
# Secure: Proper Flask debug mode configuration

from flask import Flask, request, jsonify
import os
import logging
from logging.handlers import RotatingFileHandler

# Safe: Debug mode properly controlled
app = Flask(__name__)

# Safe: Environment-based configuration with secure defaults
class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
    DEBUG = False  # Safe: Debug disabled by default
    TESTING = False
    LOG_LEVEL = logging.INFO

class DevelopmentConfig(Config):
    DEBUG = True  # Only enabled in development
    LOG_LEVEL = logging.DEBUG

class ProductionConfig(Config):
    DEBUG = False  # Explicitly disabled in production
    LOG_LEVEL = logging.WARNING
    # Additional production-specific settings
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    PERMANENT_SESSION_LIFETIME = 1800

class TestingConfig(Config):
    TESTING = True
    DEBUG = False  # Debug disabled in testing too
    WTF_CSRF_ENABLED = False

# Safe: Configuration mapping
config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': ProductionConfig  # Safe default
}

def create_app(config_name=None):
    app = Flask(__name__)
    
    # Safe: Use environment variable with secure default
    if config_name is None:
        config_name = os.environ.get('FLASK_ENV', 'production')
    
    # Load configuration
    app.config.from_object(config[config_name])
    
    # Safe: Setup logging instead of debug mode
    setup_logging(app)
    
    # Safe: Custom error handlers
    setup_error_handlers(app)
    
    return app

def setup_logging(app):
    if not app.debug and not app.testing:
        # Production logging setup
        if not os.path.exists('logs'):
            os.mkdir('logs')
        
        file_handler = RotatingFileHandler(
            'logs/app.log',
            maxBytes=10240000,
            backupCount=10
        )
        
        file_handler.setFormatter(logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
        ))
        
        file_handler.setLevel(app.config['LOG_LEVEL'])
        app.logger.addHandler(file_handler)
        app.logger.setLevel(app.config['LOG_LEVEL'])
        app.logger.info('Application startup')

def setup_error_handlers(app):
    @app.errorhandler(404)
    def not_found_error(error):
        return jsonify({'error': 'Resource not found'}), 404
    
    @app.errorhandler(500)
    def internal_error(error):
        # Safe: Log detailed error but don't expose to user
        app.logger.error(f'Server Error: {error}', exc_info=True)
        
        if app.debug:
            # Only in development: show detailed error
            import traceback
            return jsonify({
                'error': 'Internal server error',
                'details': str(error),
                'traceback': traceback.format_exc()
            }), 500
        else:
            # Production: generic error message
            return jsonify({'error': 'Internal server error'}), 500
    
    @app.errorhandler(Exception)
    def handle_exception(error):
        # Log the error
        app.logger.error(f'Unhandled exception: {error}', exc_info=True)
        
        if app.debug:
            # Re-raise in development for debugger
            raise error
        else:
            # Return generic error in production
            return jsonify({'error': 'An unexpected error occurred'}), 500

# Safe: Application routes with proper error handling
@app.route('/api/data')
def get_data():
    try:
        # Application logic
        data = process_user_data(request.args)
        return jsonify(data)
    except ValueError as e:
        # Safe: Log error, return user-friendly message
        app.logger.warning(f'Invalid data request: {e}')
        return jsonify({'error': 'Invalid request parameters'}), 400
    except Exception as e:
        # Safe: Log detailed error, return generic message
        app.logger.error(f'Data processing error: {e}', exc_info=True)
        return jsonify({'error': 'Unable to process request'}), 500

# Safe: Health check endpoint
@app.route('/health')
def health_check():
    return jsonify({
        'status': 'healthy',
        'version': os.environ.get('APP_VERSION', '1.0.0'),
        'environment': os.environ.get('FLASK_ENV', 'production')
        # Note: No sensitive configuration exposed
    })

# Safe: Debug info only in development
@app.route('/debug/info')
def debug_info():
    if not app.debug:
        return jsonify({'error': 'Debug information not available'}), 403
    
    # Safe: Limited debug info even in development
    import sys
    return jsonify({
        'debug_mode': app.debug,
        'python_version': sys.version.split()[0],
        'flask_env': os.environ.get('FLASK_ENV'),
        'app_config': {
            'DEBUG': app.config['DEBUG'],
            'TESTING': app.config['TESTING']
            # Note: Don't expose sensitive config like SECRET_KEY
        }
    })

# Safe: Configuration validation
def validate_config(app):
    """Validate critical configuration settings"""
    
    # Check if debug is disabled in production
    if os.environ.get('FLASK_ENV') == 'production' and app.debug:
        raise RuntimeError('Debug mode cannot be enabled in production')
    
    # Check for secure secret key in production
    if (os.environ.get('FLASK_ENV') == 'production' and 
        app.config['SECRET_KEY'] in ['dev', 'development', 'you-will-never-guess']):
        raise RuntimeError('Insecure SECRET_KEY in production')
    
    # Validate other security settings
    if os.environ.get('FLASK_ENV') == 'production':
        required_secure_settings = {
            'SESSION_COOKIE_SECURE': True,
            'SESSION_COOKIE_HTTPONLY': True
        }
        
        for setting, expected_value in required_secure_settings.items():
            if app.config.get(setting) != expected_value:
                app.logger.warning(f'Insecure setting: {setting} should be {expected_value}')

# Safe: Application factory with validation
def create_validated_app(config_name=None):
    app = create_app(config_name)
    
    # Validate configuration before starting
    validate_config(app)
    
    return app

# Safe: Application startup
if __name__ == '__main__':
    # Safe: Environment-controlled execution
    flask_env = os.environ.get('FLASK_ENV', 'production')
    
    app = create_validated_app(flask_env)
    
    if flask_env == 'development':
        # Development server with debug
        app.run(debug=True, host='127.0.0.1', port=5000)
    else:
        # Production: use proper WSGI server
        app.logger.warning('Use a production WSGI server like Gunicorn in production')
        app.run(debug=False, host='127.0.0.1', port=5000)

# Safe: Docker/deployment configuration
# In docker-compose.yml or deployment scripts:
# environment:
#   - FLASK_ENV=production
#   - SECRET_KEY=${SECRET_KEY}
#   - DATABASE_URL=${DATABASE_URL}

# Safe: Gunicorn configuration (gunicorn.conf.py)
# bind = "0.0.0.0:5000"
# workers = 4
# worker_class = "sync"
# timeout = 30
# keepalive = 2
# max_requests = 1000
# max_requests_jitter = 100
# preload_app = True
# accesslog = "-"
# errorlog = "-"
# loglevel = "info"

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Developers enable debug mode (app.run(debug=True)) in production code. This exposes interactive debugger with code execution capabilities, detailed error pages with stack traces, source code, and environment variables. Attackers exploit debugger PIN bypass vulnerabilities or leaked PINs.

Root causes

Setting debug=True in Flask Production Applications

Developers enable debug mode (app.run(debug=True)) in production code. This exposes interactive debugger with code execution capabilities, detailed error pages with stack traces, source code, and environment variables. Attackers exploit debugger PIN bypass vulnerabilities or leaked PINs.

Using FLASK_ENV=development or FLASK_DEBUG=1 in Production

Environment variables enable debug mode: FLASK_ENV=development or FLASK_DEBUG=1 set in production environments. These override application settings, activating Werkzeug debugger. Configuration management errors or containerization defaults inadvertently deploy debug-enabled applications exposing interactive debugger interfaces.

Failing to Disable Debug Mode Before Deployment

Development configurations remain unchanged during deployment. app.debug=True or debug parameter in app.run() not removed or conditioned on environment checks. Lack of environment-specific configuration files or deployment checklists results in debug mode running in production.

Using Flask Defaults Without Explicit Configuration

Flask's development server defaults can enable debug features when app.run() called without parameters. Developers rely on defaults during testing, forgetting to explicitly set debug=False. Production deployments using development server configurations inherit debug-enabled settings exposing debugger.

Conditional Logic Errors Enabling Debug in Production

Incorrect environment detection logic enables debug mode unintentionally: if os.environ.get('PRODUCTION') != 'true': debug=True. Typos, logic errors, or missing environment variables cause production systems to run debug-enabled. Boolean conversion errors also activate debug mode.

Fixes

1

Always Set debug=False Explicitly in Production Code

Explicitly configure app.run(debug=False) or app.debug=False in code. Use environment-based configuration: app.run(debug=os.environ.get('FLASK_ENV') == 'development'). Never rely on defaults. Ensure production code paths cannot enable debug mode regardless of environment variables.

2

Use Production-Grade WSGI Servers, Never Development Server

Deploy with Gunicorn, uWSGI, or mod_wsgi instead of Flask's development server. Production WSGI servers don't support debug mode: gunicorn 'app:app' --workers 4. Remove app.run() from production code entirely. Development server is single-threaded and insecure.

3

Configure Environment-Specific Settings Files

Create separate config classes: ProductionConfig, DevelopmentConfig. Use app.config.from_object(os.environ.get('APP_CONFIG', 'ProductionConfig')). Ensure ProductionConfig has DEBUG=False hardcoded. Implement configuration validation that fails startup if debug enabled in production.

4

Set FLASK_ENV=production and FLASK_DEBUG=0 Explicitly

Configure environment variables in deployment: FLASK_ENV=production, FLASK_DEBUG=0. These override code settings. Use container orchestration or platform settings to enforce: ENV FLASK_ENV=production in Dockerfile. Validate in application startup with assertions.

5

Implement Startup Validation Checks for Debug Mode

Add initialization code that checks debug status: if app.debug and os.environ.get('ENVIRONMENT') == 'production': raise RuntimeError('Debug enabled in production'). Fail fast during startup rather than exposing vulnerabilities. Include in health check endpoints.

6

Use Security Scanning in CI/CD to Detect Debug Mode

Integrate Bandit security scanner: bandit -r . to detect debug=True patterns. Add pre-deployment checks: grep -r 'debug=True' or parse AST for debug configurations. Fail builds if debug mode detected. Use static analysis tools in continuous integration pipelines.

Detect This Vulnerability in Your Code

Sourcery automatically identifies flask debug mode enabled in production and many other security issues in your codebase.