Files

150 lines
5.3 KiB
Python
Raw Permalink Normal View History

2026-01-27 18:04:50 -06:00
"""
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