| | """ |
| | Security utilities for the application. |
| | """ |
| |
|
| | import hashlib |
| | import secrets |
| | from typing import List, Optional |
| |
|
| | from app.core.logging import get_logger |
| |
|
| | logger = get_logger(__name__) |
| |
|
| | |
| | MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024 |
| |
|
| | |
| | ALLOWED_MIME_TYPES = [ |
| | "image/jpeg", |
| | "image/png", |
| | "image/gif", |
| | "image/webp", |
| | "image/bmp" |
| | ] |
| |
|
| | |
| | IMAGE_SIGNATURES = { |
| | b'\xff\xd8\xff': 'image/jpeg', |
| | b'\x89PNG\r\n\x1a\n': 'image/png', |
| | b'GIF87a': 'image/gif', |
| | b'GIF89a': 'image/gif', |
| | b'RIFF': 'image/webp', |
| | b'BM': 'image/bmp' |
| | } |
| |
|
| |
|
| | def validate_file_size(content: bytes, max_size: int = MAX_FILE_SIZE_BYTES) -> bool: |
| | """ |
| | Validate that file size is within allowed limits. |
| | |
| | Args: |
| | content: File content as bytes |
| | max_size: Maximum allowed size in bytes |
| | |
| | Returns: |
| | True if valid, False otherwise |
| | """ |
| | return len(content) <= max_size |
| |
|
| |
|
| | def detect_mime_type(content: bytes) -> Optional[str]: |
| | """ |
| | Detect MIME type from file content using magic bytes. |
| | |
| | Args: |
| | content: File content as bytes |
| | |
| | Returns: |
| | Detected MIME type or None |
| | """ |
| | for signature, mime_type in IMAGE_SIGNATURES.items(): |
| | if content.startswith(signature): |
| | return mime_type |
| | return None |
| |
|
| |
|
| | def validate_image_content( |
| | content: bytes, |
| | allowed_types: List[str] = ALLOWED_MIME_TYPES |
| | ) -> bool: |
| | """ |
| | Validate image content by checking magic bytes. |
| | |
| | Args: |
| | content: File content as bytes |
| | allowed_types: List of allowed MIME types |
| | |
| | Returns: |
| | True if valid image type, False otherwise |
| | """ |
| | detected_type = detect_mime_type(content) |
| | if detected_type is None: |
| | return False |
| | return detected_type in allowed_types |
| |
|
| |
|
| | def compute_file_hash(content: bytes, algorithm: str = "sha256") -> str: |
| | """ |
| | Compute hash of file content. |
| | |
| | Args: |
| | content: File content as bytes |
| | algorithm: Hash algorithm (sha256, md5, etc.) |
| | |
| | Returns: |
| | Hex-encoded hash string |
| | """ |
| | hasher = hashlib.new(algorithm) |
| | hasher.update(content) |
| | return hasher.hexdigest() |
| |
|
| |
|
| | def generate_request_id() -> str: |
| | """ |
| | Generate a unique request ID. |
| | |
| | Returns: |
| | Random hex string |
| | """ |
| | return secrets.token_hex(8) |
| |
|
| |
|
| | def sanitize_filename(filename: str) -> str: |
| | """ |
| | Sanitize a filename to prevent path traversal. |
| | |
| | Args: |
| | filename: Original filename |
| | |
| | Returns: |
| | Sanitized filename |
| | """ |
| | |
| | sanitized = filename.replace("/", "_").replace("\\", "_").replace("\x00", "") |
| | |
| | sanitized = sanitized.lstrip(".") |
| | return sanitized[:255] if sanitized else "unnamed" |
| |
|