Add request size limits - Prevents memory exhaustion from large uploads
This comprehensive request size limiting system prevents memory exhaustion and DoS attacks from oversized requests. Key features: - Global request size limit: 50MB (configurable) - Type-specific limits: 25MB for audio, 1MB for JSON, 10MB for images - Multi-layer validation before loading data into memory - File type detection based on extensions - Endpoint-specific limit enforcement - Dynamic configuration via admin API - Clear error messages with size information Implementation details: - RequestSizeLimiter middleware with Flask integration - Pre-request validation using Content-Length header - File size checking for multipart uploads - JSON payload size validation - Custom decorator for route-specific limits - StreamSizeLimiter for chunked transfers - Integration with Flask's MAX_CONTENT_LENGTH Admin features: - GET /admin/size-limits - View current limits - POST /admin/size-limits - Update limits dynamically - Human-readable size formatting in responses - Size limit info in health check endpoints Security benefits: - Prevents memory exhaustion attacks - Blocks oversized uploads before processing - Protects against buffer overflow attempts - Works with rate limiting for comprehensive protection This addresses the critical security issue of unbounded request sizes that could lead to memory exhaustion or system crashes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
108
app.py
108
app.py
@@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
from config import init_app as init_config
|
||||
from secrets_manager import init_app as init_secrets
|
||||
from session_manager import init_app as init_session_manager, track_resource
|
||||
from request_size_limiter import RequestSizeLimiter, limit_request_size
|
||||
|
||||
# Error boundary decorator for Flask routes
|
||||
def with_error_boundary(func):
|
||||
@@ -110,6 +111,14 @@ app.config['UPLOAD_FOLDER'] = upload_folder
|
||||
# Initialize session management after upload folder is configured
|
||||
init_session_manager(app)
|
||||
|
||||
# Initialize request size limiter
|
||||
request_size_limiter = RequestSizeLimiter(app, {
|
||||
'max_content_length': app.config.get('MAX_CONTENT_LENGTH', 50 * 1024 * 1024), # 50MB default
|
||||
'max_audio_size': app.config.get('MAX_AUDIO_SIZE', 25 * 1024 * 1024), # 25MB for audio
|
||||
'max_json_size': app.config.get('MAX_JSON_SIZE', 1 * 1024 * 1024), # 1MB for JSON
|
||||
'max_image_size': app.config.get('MAX_IMAGE_SIZE', 10 * 1024 * 1024), # 10MB for images
|
||||
})
|
||||
|
||||
# TTS configuration is already loaded from config.py
|
||||
# Warn if TTS API key is not set
|
||||
if not app.config.get('TTS_API_KEY'):
|
||||
@@ -592,6 +601,7 @@ def index():
|
||||
|
||||
@app.route('/transcribe', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=10, requests_per_hour=100, check_size=True)
|
||||
@limit_request_size(max_audio_size=25 * 1024 * 1024) # 25MB limit for audio
|
||||
@with_error_boundary
|
||||
@track_resource('audio_file')
|
||||
def transcribe():
|
||||
@@ -707,6 +717,7 @@ def transcribe():
|
||||
|
||||
@app.route('/translate', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=20, requests_per_hour=300, check_size=True)
|
||||
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||
@with_error_boundary
|
||||
def translate():
|
||||
try:
|
||||
@@ -775,6 +786,7 @@ def translate():
|
||||
|
||||
@app.route('/translate/stream', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=10, requests_per_hour=150, check_size=True)
|
||||
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||
@with_error_boundary
|
||||
def translate_stream():
|
||||
"""Streaming translation endpoint for reduced latency"""
|
||||
@@ -872,6 +884,7 @@ def translate_stream():
|
||||
|
||||
@app.route('/speak', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=15, requests_per_hour=200, check_size=True)
|
||||
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||
@with_error_boundary
|
||||
@track_resource('audio_file')
|
||||
def speak():
|
||||
@@ -1127,6 +1140,13 @@ def detailed_health_check():
|
||||
health_status['metrics']['uptime'] = time.time() - app.start_time if hasattr(app, 'start_time') else 0
|
||||
health_status['metrics']['request_count'] = getattr(app, 'request_count', 0)
|
||||
|
||||
# Add size limits info
|
||||
if hasattr(app, 'request_size_limiter'):
|
||||
health_status['metrics']['size_limits'] = {
|
||||
k: f"{v / 1024 / 1024:.1f}MB" if v > 1024 * 1024 else f"{v / 1024:.1f}KB"
|
||||
for k, v in app.request_size_limiter.limits.items()
|
||||
}
|
||||
|
||||
# Set appropriate HTTP status code
|
||||
http_status = 200 if health_status['status'] == 'healthy' else 503 if health_status['status'] == 'unhealthy' else 200
|
||||
|
||||
@@ -1481,5 +1501,93 @@ def get_session_metrics():
|
||||
logger.error(f"Failed to get session metrics: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/admin/size-limits', methods=['GET'])
|
||||
@rate_limit(requests_per_minute=10)
|
||||
def get_size_limits():
|
||||
"""Get current request size limits"""
|
||||
try:
|
||||
# Simple authentication check
|
||||
auth_token = request.headers.get('X-Admin-Token')
|
||||
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||
|
||||
if auth_token != expected_token:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
if hasattr(app, 'request_size_limiter'):
|
||||
return jsonify({
|
||||
'limits': app.request_size_limiter.limits,
|
||||
'limits_human': {
|
||||
k: f"{v / 1024 / 1024:.1f}MB" if v > 1024 * 1024 else f"{v / 1024:.1f}KB"
|
||||
for k, v in app.request_size_limiter.limits.items()
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({'error': 'Size limiter not initialized'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get size limits: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/admin/size-limits', methods=['POST'])
|
||||
@rate_limit(requests_per_minute=5)
|
||||
def update_size_limits():
|
||||
"""Update request size limits"""
|
||||
try:
|
||||
# Simple authentication check
|
||||
auth_token = request.headers.get('X-Admin-Token')
|
||||
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||
|
||||
if auth_token != expected_token:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({'error': 'No data provided'}), 400
|
||||
|
||||
# Validate limits
|
||||
valid_keys = {'max_content_length', 'max_audio_size', 'max_json_size', 'max_image_size'}
|
||||
updates = {}
|
||||
|
||||
for key, value in data.items():
|
||||
if key in valid_keys:
|
||||
try:
|
||||
# Accept values in MB and convert to bytes
|
||||
if isinstance(value, str) and value.endswith('MB'):
|
||||
value = float(value[:-2]) * 1024 * 1024
|
||||
elif isinstance(value, str) and value.endswith('KB'):
|
||||
value = float(value[:-2]) * 1024
|
||||
else:
|
||||
value = int(value)
|
||||
|
||||
# Enforce reasonable limits
|
||||
if value < 1024: # Minimum 1KB
|
||||
return jsonify({'error': f'{key} too small (min 1KB)'}), 400
|
||||
if value > 500 * 1024 * 1024: # Maximum 500MB
|
||||
return jsonify({'error': f'{key} too large (max 500MB)'}), 400
|
||||
|
||||
updates[key] = value
|
||||
except ValueError:
|
||||
return jsonify({'error': f'Invalid value for {key}'}), 400
|
||||
|
||||
if not updates:
|
||||
return jsonify({'error': 'No valid limits provided'}), 400
|
||||
|
||||
# Update limits
|
||||
old_limits = app.request_size_limiter.update_limits(**updates)
|
||||
|
||||
logger.info(f"Size limits updated by admin: {updates}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'old_limits': old_limits,
|
||||
'new_limits': app.request_size_limiter.limits,
|
||||
'new_limits_human': {
|
||||
k: f"{v / 1024 / 1024:.1f}MB" if v > 1024 * 1024 else f"{v / 1024:.1f}KB"
|
||||
for k, v in app.request_size_limiter.limits.items()
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update size limits: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5005, debug=True)
|
||||
|
||||
Reference in New Issue
Block a user