Skip to content

CWE-209: Error Message Information Leak - Python

Overview

Error Message Information Leak in Python applications commonly occurs when exception details, stack traces, or debug information are returned to users through web responses, API outputs, or error pages. Python's detailed tracebacks are extremely helpful for debugging but reveal sensitive information including file paths, code structure, library versions, SQL queries, and internal logic to attackers.

Primary Defence: Return generic error messages to users while logging detailed exceptions server-side, set DEBUG = False in Django production, and use custom error handlers in Flask.

Common Vulnerable Patterns

Returning Raw Exception Messages to Users

# VULNERABLE - Exposes database errors and SQL queries
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/user/<int:user_id>')
def get_user(user_id):
    try:
        user = db.execute(f"SELECT * FROM users WHERE id = {user_id}")
        return jsonify(user)
    except Exception as e:
        # Returns: "no such table: users" or SQL syntax errors
        return jsonify({'error': str(e)}), 500

Why this is vulnerable:

  • Exposes table/column names and SQL syntax errors.
  • Reveals database vendor details and constraint names.
  • Maps schema structure and data relationships.
  • Enables targeted SQL injection planning.

Exposing Full Stack Traces in Responses

# VULNERABLE - Returns complete traceback with file paths
import traceback
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/process')
def process_data():
    try:
        result = perform_complex_operation()
        return jsonify(result)
    except Exception as e:
        # Exposes: file paths, code structure, library versions
        return jsonify({
            'error': str(e),
            'traceback': traceback.format_exc()
        }), 500

Why this is vulnerable:

  • Exposes file paths, function names, and line numbers.
  • Reveals module imports and package versions.
  • Maps internal code flow and structure.
  • Helps attackers target specific modules.

Debug Mode Enabled in Production

# VULNERABLE - Flask debug mode shows interactive debugger
from flask import Flask

app = Flask(__name__)
app.config['DEBUG'] = True  # NEVER in production!

if __name__ == '__main__':
    app.run(debug=True)  # Interactive debugger with code execution!

Why this is vulnerable:

  • Werkzeug debugger enables remote code execution.
  • Attackers can run shell commands from the browser.
  • Exposes environment variables and database access.
  • A single error can lead to full compromise.

Verbose Error Messages with Internal Details

# VULNERABLE - Reveals file paths and internal logic
from flask import Flask, request, jsonify

app = Flask(__name__)
@app.route('/upload', methods=['POST'])
def upload_file():
    try:
        file = request.files['document']
        file.save(f'/var/www/uploads/{file.filename}')
        return jsonify({'status': 'success'})
    except Exception as e:
        # Returns: "Permission denied: /var/www/uploads/file.txt"
        return jsonify({'error': f'Failed to save file: {str(e)}'}), 500

Why this is vulnerable:

  • Leaks internal paths and filesystem layout.
  • Exposes permission details and upload handling logic.
  • Helps identify traversal or overwrite targets.
  • Reveals web root and storage locations.

Logging Sensitive Data to User-Accessible Locations

# VULNERABLE - Logs passwords and tokens
import logging
from flask import Flask, request, jsonify

app = Flask(__name__)

logging.basicConfig(filename='app.log', level=logging.DEBUG)

@app.route('/login', methods=['POST'])
def login():
    username = request.json['username']
    password = request.json['password']

    # Logs plaintext password!
    logging.info(f'Login attempt: {username}, {password}')

    if authenticate(username, password):
        return jsonify({'token': generate_token()})

Why this is vulnerable:

  • Logs plaintext passwords, tokens, and API keys.
  • Logs can be accessible via aggregation or backups.
  • Overbroad access exposes credentials to more users.
  • Enables account takeover without alerts.

Secure Patterns

Generic Error Messages with Server-Side Logging

# SECURE - Generic user errors, detailed server logs
from flask import Flask, jsonify
import logging

app = Flask(__name__)

logging.basicConfig(
    filename='/var/log/myapp/errors.log',
    level=logging.ERROR,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

@app.route('/user/<int:user_id>')
def get_user(user_id):
    try:
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'Resource not found'}), 404
        return jsonify(user.to_dict())
    except Exception as e:
        # Log full details server-side
        logging.error(f'Error fetching user {user_id}: {str(e)}', exc_info=True)
        # Return generic message to user
        return jsonify({'error': 'An error occurred processing your request'}), 500

Why this works:

  • Full tracebacks are logged server-side only.
  • Clients receive generic messages with no internals.
  • Logs are stored outside the web root.
  • ORM lookups avoid SQL injection and noisy errors.

Custom Error Handler with Error Codes

# SECURE - Error codes for tracking, no sensitive details
from flask import Flask, jsonify
import uuid
import logging

app = Flask(__name__)

@app.errorhandler(Exception)
def handle_error(error):
    # Generate unique error ID for tracking
    error_id = str(uuid.uuid4())

    # Log with error ID and full details
    logging.error(f'Error ID {error_id}: {str(error)}', exc_info=True)

    # Return generic message with tracking ID
    return jsonify({
        'error': 'An error occurred',
        'error_id': error_id,
        'message': 'Please contact support with this error ID'
    }), 500

@app.errorhandler(404)
def handle_not_found(error):
    return jsonify({'error': 'Resource not found'}), 404

Why this works:

  • Global handler catches unhandled exceptions.
  • Error IDs correlate client reports to server logs.
  • Full tracebacks stay server-side with exc_info=True.
  • 404s are handled separately without leaking details.

Django Production Settings

# SECURE - Django production configuration
# settings.py

DEBUG = False  # CRITICAL: Must be False in production
ALLOWED_HOSTS = ['yourdomain.com']

# Custom error handlers
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/error.log',
            'formatter': 'verbose',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}

# Custom error views
# views.py
from django.http import JsonResponse

def custom_500(request):
    return JsonResponse({
        'error': 'Internal server error',
        'message': 'Please try again later'
    }, status=500)

# urls.py
handler500 = 'myapp.views.custom_500'

Why this works:

  • DEBUG = False disables detailed error pages.
  • ALLOWED_HOSTS blocks host header abuse.
  • Errors are logged server-side with secure file paths.
  • Custom handlers return generic 500 responses.
  • Global handler prevents accidental traceback leaks.

FastAPI Exception Handlers

# SECURE - FastAPI with custom exception handlers
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import logging
import uuid

app = FastAPI()

logging.basicConfig(
    filename='/var/log/fastapi/app.log',
    level=logging.ERROR,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
    error_id = str(uuid.uuid4())

    # Log full error details
    logging.error(
        f'Error ID {error_id}: {str(exc)}',
        exc_info=True,
        extra={'path': request.url.path}
    )

    # Return generic error
    return JSONResponse(
        status_code=500,
        content={
            'error': 'Internal server error',
            'error_id': error_id
        }
    )

@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    return JSONResponse(
        status_code=exc.status_code,
        content={'error': exc.detail}
    )

Why this works:

  • Global handler sanitizes unexpected exceptions.
  • Error IDs correlate client reports to logs.
  • Full tracebacks are logged with exc_info=True.
  • HTTPException responses preserve intended status codes.

Structured Logging with Redaction

# SECURE - Log filtering to redact sensitive data
import logging
import re
from flask import Flask, request, jsonify

app = Flask(__name__)

class SensitiveDataFilter(logging.Filter):
    """Filter to redact sensitive data from logs"""

    SENSITIVE_PATTERNS = [
        (r'password["\']?\s*[:=]\s*["\']?([^"\'}\s]+)', r'password=***REDACTED***'),
        (r'token["\']?\s*[:=]\s*["\']?([^"\'}\s]+)', r'token=***REDACTED***'),
        (r'\b\d{13,19}\b', r'***CARD***'),  # Credit card numbers
        (r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', r'***EMAIL***'),
    ]

    def filter(self, record):
        message = record.getMessage()
        for pattern, replacement in self.SENSITIVE_PATTERNS:
            message = re.sub(pattern, replacement, message, flags=re.IGNORECASE)
        record.msg = message
        record.args = ()
        return True

# Configure logging with filter
logger = logging.getLogger('myapp')
logger.addFilter(SensitiveDataFilter())

# Usage
@app.route('/process', methods=['POST'])
def process_payment():
    try:
        data = request.json
        # Log request (sensitive data will be redacted)
        logger.info(f'Processing payment: {data}')
        return jsonify({'status': 'success'})
    except Exception as e:
        logger.error(f'Payment processing failed: {str(e)}', exc_info=True)
        return jsonify({'error': 'Payment processing failed'}), 500

Why this works:

  • Filters redact secrets before logs are written.
  • Regex patterns catch common credential formats.
  • Case-insensitive matching covers variants.
  • Clears original args to avoid unredacted output.
  • Centralized filter protects all loggers.

Environment-Aware Error Handling

# SECURE - Different error handling for dev vs production
import os
from flask import Flask, jsonify
import logging

app = Flask(__name__)
IS_PRODUCTION = os.getenv('ENV') == 'production'

@app.errorhandler(Exception)
def handle_exception(error):
    # Log full details in all environments
    logging.error(f'Error: {str(error)}', exc_info=True)

    if IS_PRODUCTION:
        # Generic error in production
        return jsonify({
            'error': 'An error occurred',
            'message': 'Please try again later'
        }), 500
    else:
        # Detailed error in development only
        return jsonify({
            'error': str(error),
            'type': type(error).__name__
        }), 500

Why this works:

  • Production responses are generic and non-verbose.
  • Development responses retain details for debugging.
  • Full errors are always logged server-side.
  • Environment check is server-controlled.

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Additional Resources