# SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2025 Allaun """Lightweight database security module for local file management. This module provides essential security features for the local SQLite database: - Input validation and sanitization - SQL injection prevention - Path validation for file operations - Secure error handling """ import re import os from typing import Any, Optional from pathlib import Path class SecurityError(Exception): """Base security exception.""" pass class InputValidationError(SecurityError): """Input validation failed exception.""" pass class DatabaseSecurity: """Lightweight database security for local file management. Provides security utilities for validating input, preventing SQL injection, and securing file operations. """ def __init__(self, db): """Initialize database security. Args: db: Database connection or instance """ self.db = db def validate_input(self, data: Any, data_type: Optional[str] = None) -> bool: """Validate input data for database operations. Args: data: Input data to validate data_type: Expected data type (optional) Returns: True if validation passes Raises: InputValidationError: If validation fails """ if data is None: raise InputValidationError("Input data cannot be None") # Type checking if specified - use safe type lookup instead of eval if data_type: # Safe type mapping - only allow known types safe_types = { 'str': str, 'int': int, 'float': float, 'bool': bool, 'list': list, 'dict': dict, 'tuple': tuple, 'set': set, 'bytes': bytes, 'bytearray': bytearray } expected_type = safe_types.get(data_type) if expected_type is None: raise InputValidationError(f"Unknown type: {data_type}") if not isinstance(data, expected_type): raise InputValidationError(f"Expected {data_type}, got {type(data)}") # String validation if isinstance(data, str): if not self._is_safe_string(data): raise InputValidationError("String contains potentially dangerous content") return True def _is_safe_string(self, value: str) -> bool: """Check if a string is safe for database operations. Args: value: String to validate Returns: True if string is safe """ if not value: return True # Check for SQL injection patterns dangerous_patterns = [ r'--', r';', r'/\*', r'\*/', r'xp_', r'exec\(', r'union\s+select', r'drop\s+table', r'insert\s+into', r'delete\s+from', r'update\s+.*set', r'select\s+.*from', r'or\s+1=1' ] for pattern in dangerous_patterns: if re.search(pattern, value, re.IGNORECASE): return False return True def validate_path(self, path: str, base_dir: Optional[str] = None) -> bool: """Validate file path to prevent directory traversal attacks. Args: path: File path to validate base_dir: Base directory to restrict paths to Returns: True if path is valid Raises: InputValidationError: If path is invalid """ if not path: raise InputValidationError("Path cannot be empty") try: # Convert to absolute path abs_path = os.path.abspath(path) # Reject obvious traversal components if '..' in Path(path).parts: raise InputValidationError("Path contains directory traversal components") # If base_dir is specified, ensure path is within it if base_dir: base_abs = os.path.abspath(base_dir) # Normalize both paths and compare prefix if not os.path.commonpath([base_abs, abs_path]) == base_abs: raise InputValidationError(f"Path must be within {base_dir}") return True except InputValidationError: raise except Exception as e: raise InputValidationError(f"Path validation failed: {e}") def sanitize_error_message(self, error: Exception) -> str: """Sanitize error messages to prevent information leakage. Args: error: Exception to sanitize Returns: Sanitized error message """ # Get error message error_msg = str(error) # Remove sensitive information sensitive_patterns = [ r'PASSWORD_REMOVED', r'SECRET_REMOVED', r'key', r'TOKEN_REMOVED', r'api[_-]?key', r'connection[_-]?string', r'database[_-]?url', r'user[_-]?name' ] for pattern in sensitive_patterns: error_msg = re.sub(pattern, '[REDACTED]', error_msg, flags=re.IGNORECASE) # Remove stack traces and file paths error_msg = re.sub(r'File ".*?", line \d+', '[REDACTED]', error_msg) error_msg = re.sub(r'Traceback.*?\n', '', error_msg) return error_msg def validate_identifier(self, identifier: str) -> bool: """Validate SQL identifier (table/column name). Args: identifier: SQL identifier to validate Returns: True if identifier is valid Raises: InputValidationError: If identifier is invalid """ if not identifier or not isinstance(identifier, str): raise InputValidationError("Identifier cannot be empty") # Only allow alphanumeric and underscore, must start with letter if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', identifier): raise InputValidationError(f"Invalid identifier: {identifier}") return True def validate_schema(self, schema: str) -> bool: """Validate schema definition. Args: schema: Schema definition to validate Returns: True if schema is valid Raises: InputValidationError: If schema is invalid """ if not schema or not isinstance(schema, str): raise InputValidationError("Schema cannot be empty") # Basic validation - should contain valid column definitions if not re.match(r'^[a-zA-Z0-9_,\s\(\)]+$', schema): raise InputValidationError(f"Invalid schema definition: {schema}") return True