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 = Falsedisables detailed error pages.ALLOWED_HOSTSblocks 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