""" Input validation schemas using basic validation For production, consider migrating to Pydantic for comprehensive validation """ import re import bleach from functools import wraps from flask import request, jsonify class ValidationError(Exception): """Custom validation error""" pass def sanitize_html(text): """ Sanitize HTML to prevent XSS attacks Allows only safe tags and attributes """ if not text: return text # Allow minimal formatting tags allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'ul', 'ol', 'pre', 'code'] allowed_attributes = {} return bleach.clean( text, tags=allowed_tags, attributes=allowed_attributes, strip=True ) def validate_string(value, field_name, min_length=None, max_length=None, pattern=None, required=True): """Validate string field""" if value is None or value == '': if required: raise ValidationError(f"{field_name} is required") return True if not isinstance(value, str): raise ValidationError(f"{field_name} must be a string") if min_length and len(value) < min_length: raise ValidationError(f"{field_name} must be at least {min_length} characters") if max_length and len(value) > max_length: raise ValidationError(f"{field_name} must not exceed {max_length} characters") if pattern and not re.match(pattern, value): raise ValidationError(f"{field_name} has invalid format") return True def validate_email(email, required=False): """Validate email format""" if not email and not required: return True pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if not re.match(pattern, email): raise ValidationError("Invalid email format") return True def sanitize_filename(filename): """Sanitize filename to prevent path traversal""" # Remove any path separators filename = filename.replace('..', '').replace('/', '').replace('\\', '') # Allow only alphanumeric, dash, underscore, and dot filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename) return filename[:255] # Limit length def validate_uuid(value): """Validate UUID format""" uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if not re.match(uuid_pattern, str(value).lower()): raise ValidationError("Invalid UUID format") return True # Request validation schemas PROFILE_SCHEMA = { 'name': {'type': 'string', 'required': True, 'min_length': 1, 'max_length': 255}, 'email': {'type': 'email', 'required': False}, 'contact_number': {'type': 'string', 'required': False, 'max_length': 50}, 'default_key': {'type': 'string', 'required': False, 'max_length': 10}, 'notes': {'type': 'string', 'required': False, 'max_length': 5000} } SONG_SCHEMA = { 'title': {'type': 'string', 'required': True, 'min_length': 1, 'max_length': 500}, 'artist': {'type': 'string', 'required': False, 'max_length': 500}, 'band': {'type': 'string', 'required': False, 'max_length': 500}, 'singer': {'type': 'string', 'required': False, 'max_length': 500}, 'lyrics': {'type': 'string', 'required': False, 'max_length': 50000}, 'chords': {'type': 'string', 'required': False, 'max_length': 50000} } PLAN_SCHEMA = { 'name': {'type': 'string', 'required': True, 'min_length': 1, 'max_length': 500}, 'date': {'type': 'string', 'required': False}, # Will validate format separately 'notes': {'type': 'string', 'required': False, 'max_length': 5000} } def validate_request_data(schema): """ Decorator to validate request JSON data against schema Usage: @app.route('/api/profiles', methods=['POST']) @validate_request_data(PROFILE_SCHEMA) def create_profile(): data = request.get_json() ... """ def decorator(f): @wraps(f) def wrapped(*args, **kwargs): if not request.is_json: return jsonify({'error': 'Content-Type must be application/json'}), 400 data = request.get_json() if not data: return jsonify({'error': 'Request body is required'}), 400 try: # Validate each field in schema for field, rules in schema.items(): value = data.get(field) if rules['type'] == 'string': validate_string( value, field, min_length=rules.get('min_length'), max_length=rules.get('max_length'), pattern=rules.get('pattern'), required=rules.get('required', False) ) elif rules['type'] == 'email': if value: validate_email(value, required=rules.get('required', False)) return f(*args, **kwargs) except ValidationError as e: return jsonify({'error': 'validation_error', 'message': str(e)}), 400 return wrapped return decorator