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.