""" Comprehensive input validation and sanitization utilities Provides validation for user input formats, constraints, and security """ import json import logging import re from datetime import datetime, date from pathlib import Path from typing import Any, Dict, List, Optional, Union, Tuple, Callable from urllib.parse import urlparse try: from .error_handling import ValidationError except ImportError: from error_handling import ValidationError class InputValidator: """Comprehensive input validation utility class""" def __init__(self): self.logger = logging.getLogger(__name__) # Common regex patterns self.patterns = { 'email': re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'), 'url': re.compile(r'^https?://[^\s/$.?#].[^\s]*$'), 'api_key_claude': re.compile(r'^sk-ant-[a-zA-Z0-9_-]{50,}$'), 'api_key_generic': re.compile(r'^[a-zA-Z0-9_-]{8,}$'), 'date_iso': re.compile(r'^\d{4}-\d{2}-\d{2}$'), 'datetime_iso': re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'), 'file_extension': re.compile(r'^\.[a-zA-Z0-9]+$'), 'folder_name': re.compile(r'^[a-zA-Z0-9_\-\s/]+$'), 'safe_filename': re.compile(r'^[a-zA-Z0-9_\-\.\s]+$'), } # Validation constraints self.constraints = { 'max_string_length': 10000, 'max_content_length': 1000000, # 1MB 'min_password_length': 8, 'max_api_key_length': 200, 'max_path_length': 4096, } def validate_string( self, value: Any, field_name: str, min_length: int = 0, max_length: Optional[int] = None, pattern: Optional[str] = None, allow_empty: bool = False, strip_whitespace: bool = True ) -> str: """ Validate string input with comprehensive checks Args: value: Input value to validate field_name: Name of the field for error messages min_length: Minimum string length max_length: Maximum string length pattern: Regex pattern name or custom pattern allow_empty: Whether to allow empty strings strip_whitespace: Whether to strip whitespace Returns: Validated and sanitized string Raises: ValidationError: If validation fails """ # Type check if not isinstance(value, str): if value is None: if allow_empty: return "" raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) # Try to convert to string try: value = str(value) except Exception: raise ValidationError( message=f"{field_name} must be a string", field_name=field_name, validation_rule="string_type" ) # Strip whitespace if requested if strip_whitespace: value = value.strip() # Check empty string if not value and not allow_empty: raise ValidationError( message=f"{field_name} cannot be empty", field_name=field_name, validation_rule="non_empty" ) # Length validation if len(value) < min_length: raise ValidationError( message=f"{field_name} must be at least {min_length} characters long", field_name=field_name, validation_rule="min_length" ) max_len = max_length or self.constraints['max_string_length'] if len(value) > max_len: raise ValidationError( message=f"{field_name} cannot exceed {max_len} characters", field_name=field_name, validation_rule="max_length" ) # Pattern validation if pattern and value: if pattern in self.patterns: regex = self.patterns[pattern] else: try: regex = re.compile(pattern) except re.error as e: raise ValidationError( message=f"Invalid regex pattern for {field_name}: {e}", field_name=field_name, validation_rule="pattern_error" ) if not regex.match(value): raise ValidationError( message=f"{field_name} format is invalid", field_name=field_name, validation_rule="pattern_match" ) return value def validate_integer( self, value: Any, field_name: str, min_value: Optional[int] = None, max_value: Optional[int] = None, allow_none: bool = False ) -> Optional[int]: """ Validate integer input Args: value: Input value to validate field_name: Name of the field for error messages min_value: Minimum allowed value max_value: Maximum allowed value allow_none: Whether to allow None values Returns: Validated integer or None Raises: ValidationError: If validation fails """ if value is None: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) # Try to convert to integer if isinstance(value, str): value = value.strip() if not value: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be empty", field_name=field_name, validation_rule="non_empty" ) try: int_value = int(value) except (ValueError, TypeError): raise ValidationError( message=f"{field_name} must be a valid integer", field_name=field_name, validation_rule="integer_type" ) # Range validation if min_value is not None and int_value < min_value: raise ValidationError( message=f"{field_name} must be at least {min_value}", field_name=field_name, validation_rule="min_value" ) if max_value is not None and int_value > max_value: raise ValidationError( message=f"{field_name} cannot exceed {max_value}", field_name=field_name, validation_rule="max_value" ) return int_value def validate_float( self, value: Any, field_name: str, min_value: Optional[float] = None, max_value: Optional[float] = None, allow_none: bool = False ) -> Optional[float]: """ Validate float input Args: value: Input value to validate field_name: Name of the field for error messages min_value: Minimum allowed value max_value: Maximum allowed value allow_none: Whether to allow None values Returns: Validated float or None Raises: ValidationError: If validation fails """ if value is None: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) # Try to convert to float if isinstance(value, str): value = value.strip() if not value: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be empty", field_name=field_name, validation_rule="non_empty" ) try: float_value = float(value) except (ValueError, TypeError): raise ValidationError( message=f"{field_name} must be a valid number", field_name=field_name, validation_rule="float_type" ) # Range validation if min_value is not None and float_value < min_value: raise ValidationError( message=f"{field_name} must be at least {min_value}", field_name=field_name, validation_rule="min_value" ) if max_value is not None and float_value > max_value: raise ValidationError( message=f"{field_name} cannot exceed {max_value}", field_name=field_name, validation_rule="max_value" ) return float_value def validate_boolean( self, value: Any, field_name: str, allow_none: bool = False ) -> Optional[bool]: """ Validate boolean input with flexible conversion Args: value: Input value to validate field_name: Name of the field for error messages allow_none: Whether to allow None values Returns: Validated boolean or None Raises: ValidationError: If validation fails """ if value is None: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) # Handle boolean type if isinstance(value, bool): return value # Handle string conversion if isinstance(value, str): value = value.strip().lower() if value in ('true', '1', 'yes', 'on', 'enabled'): return True elif value in ('false', '0', 'no', 'off', 'disabled'): return False else: raise ValidationError( message=f"{field_name} must be a valid boolean value (true/false, yes/no, 1/0)", field_name=field_name, validation_rule="boolean_format" ) # Handle numeric conversion if isinstance(value, (int, float)): return bool(value) raise ValidationError( message=f"{field_name} must be a valid boolean value", field_name=field_name, validation_rule="boolean_type" ) def validate_list( self, value: Any, field_name: str, min_length: int = 0, max_length: Optional[int] = None, item_validator: Optional[Callable] = None, allow_none: bool = False ) -> Optional[List[Any]]: """ Validate list input with optional item validation Args: value: Input value to validate field_name: Name of the field for error messages min_length: Minimum list length max_length: Maximum list length item_validator: Function to validate each item allow_none: Whether to allow None values Returns: Validated list or None Raises: ValidationError: If validation fails """ if value is None: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) # Convert to list if needed if not isinstance(value, list): if isinstance(value, (tuple, set)): value = list(value) else: raise ValidationError( message=f"{field_name} must be a list", field_name=field_name, validation_rule="list_type" ) # Length validation if len(value) < min_length: raise ValidationError( message=f"{field_name} must contain at least {min_length} items", field_name=field_name, validation_rule="min_length" ) if max_length is not None and len(value) > max_length: raise ValidationError( message=f"{field_name} cannot contain more than {max_length} items", field_name=field_name, validation_rule="max_length" ) # Validate each item if validator provided if item_validator: validated_items = [] for i, item in enumerate(value): try: validated_item = item_validator(item) validated_items.append(validated_item) except ValidationError as e: raise ValidationError( message=f"{field_name}[{i}]: {e.message}", field_name=f"{field_name}[{i}]", validation_rule=e.validation_rule ) return validated_items return value def validate_dict( self, value: Any, field_name: str, required_keys: Optional[List[str]] = None, optional_keys: Optional[List[str]] = None, allow_extra_keys: bool = True, allow_none: bool = False ) -> Optional[Dict[str, Any]]: """ Validate dictionary input with key validation Args: value: Input value to validate field_name: Name of the field for error messages required_keys: List of required keys optional_keys: List of optional keys allow_extra_keys: Whether to allow keys not in required/optional allow_none: Whether to allow None values Returns: Validated dictionary or None Raises: ValidationError: If validation fails """ if value is None: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) if not isinstance(value, dict): raise ValidationError( message=f"{field_name} must be a dictionary", field_name=field_name, validation_rule="dict_type" ) # Check required keys if required_keys: missing_keys = [key for key in required_keys if key not in value] if missing_keys: raise ValidationError( message=f"{field_name} is missing required keys: {', '.join(missing_keys)}", field_name=field_name, validation_rule="missing_keys" ) # Check for unexpected keys if not allow_extra_keys and (required_keys or optional_keys): allowed_keys = set(required_keys or []) | set(optional_keys or []) extra_keys = [key for key in value.keys() if key not in allowed_keys] if extra_keys: raise ValidationError( message=f"{field_name} contains unexpected keys: {', '.join(extra_keys)}", field_name=field_name, validation_rule="extra_keys" ) return value def validate_json( self, value: Any, field_name: str, allow_none: bool = False ) -> Optional[Union[Dict[str, Any], List[Any]]]: """ Validate JSON input (string or already parsed) Args: value: Input value to validate field_name: Name of the field for error messages allow_none: Whether to allow None values Returns: Parsed JSON data or None Raises: ValidationError: If validation fails """ if value is None: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be None", field_name=field_name, validation_rule="not_none" ) # If already parsed, validate it's JSON-serializable if isinstance(value, (dict, list)): try: json.dumps(value) return value except (TypeError, ValueError) as e: raise ValidationError( message=f"{field_name} contains non-JSON-serializable data: {e}", field_name=field_name, validation_rule="json_serializable" ) # If string, try to parse if isinstance(value, str): value = value.strip() if not value: if allow_none: return None raise ValidationError( message=f"{field_name} cannot be empty", field_name=field_name, validation_rule="non_empty" ) try: return json.loads(value) except json.JSONDecodeError as e: raise ValidationError( message=f"{field_name} is not valid JSON: {e}", field_name=field_name, validation_rule="json_format" ) raise ValidationError( message=f"{field_name} must be a JSON string or parsed JSON data", field_name=field_name, validation_rule="json_type" ) class ContentValidator: """Validator for content-specific inputs""" def __init__(self): self.logger = logging.getLogger(__name__) self.base_validator = InputValidator() def validate_journal_content( self, content: Any, field_name: str = "journal_content", max_length: Optional[int] = None ) -> str: """ Validate journal content with content-specific rules Args: content: Journal content to validate field_name: Name of the field for error messages max_length: Maximum content length Returns: Validated journal content Raises: ValidationError: If validation fails """ max_len = max_length or self.base_validator.constraints['max_content_length'] validated_content = self.base_validator.validate_string( content, field_name, min_length=1, max_length=max_len, allow_empty=False ) # Additional content validation if len(validated_content.strip()) < 10: raise ValidationError( message=f"{field_name} appears to be too short for meaningful analysis", field_name=field_name, validation_rule="content_too_short" ) # Check for suspicious content patterns suspicious_patterns = [ r']*>.*?', # Script tags r'javascript:', # JavaScript URLs r'data:text/html', # Data URLs ] for pattern in suspicious_patterns: if re.search(pattern, validated_content, re.IGNORECASE | re.DOTALL): self.logger.warning(f"Suspicious content pattern detected in {field_name}") # Don't reject, but log for security monitoring return validated_content def validate_api_key( self, api_key: Any, api_type: str = "generic", field_name: str = "api_key" ) -> str: """ Validate API key with type-specific rules Args: api_key: API key to validate api_type: Type of API key (claude, generic, obsidian) field_name: Name of the field for error messages Returns: Validated API key Raises: ValidationError: If validation fails """ validated_key = self.base_validator.validate_string( api_key, field_name, min_length=8, max_length=self.base_validator.constraints['max_api_key_length'], allow_empty=False ) # Type-specific validation if api_type == "claude": if not validated_key.startswith('sk-ant-'): raise ValidationError( message=f"{field_name} for Claude API should start with 'sk-ant-'", field_name=field_name, validation_rule="claude_api_key_format" ) if len(validated_key) < 50: raise ValidationError( message=f"{field_name} for Claude API appears to be too short", field_name=field_name, validation_rule="claude_api_key_length" ) elif api_type == "obsidian": # Obsidian API keys are user-generated, so less strict if len(validated_key) < 8: raise ValidationError( message=f"{field_name} for Obsidian API should be at least 8 characters", field_name=field_name, validation_rule="obsidian_api_key_length" ) # Check for obviously insecure keys insecure_keys = ['password', '123456', 'admin', 'test', 'key', 'secret'] if validated_key.lower() in insecure_keys: raise ValidationError( message=f"{field_name} appears to be insecure. Please use a stronger API key", field_name=field_name, validation_rule="insecure_api_key" ) # Check for common issues if ' ' in validated_key: raise ValidationError( message=f"{field_name} should not contain spaces", field_name=field_name, validation_rule="api_key_whitespace" ) if validated_key.endswith('...') or '...' in validated_key: raise ValidationError( message=f"{field_name} appears to be truncated", field_name=field_name, validation_rule="api_key_truncated" ) return validated_key def validate_url( self, url: Any, field_name: str = "url", allowed_schemes: Optional[List[str]] = None, require_https: bool = False ) -> str: """ Validate URL with security checks Args: url: URL to validate field_name: Name of the field for error messages allowed_schemes: List of allowed URL schemes require_https: Whether to require HTTPS Returns: Validated URL Raises: ValidationError: If validation fails """ validated_url = self.base_validator.validate_string( url, field_name, min_length=1, allow_empty=False ) # Parse URL try: parsed = urlparse(validated_url) except Exception as e: raise ValidationError( message=f"{field_name} is not a valid URL: {e}", field_name=field_name, validation_rule="url_parse_error" ) # Check scheme if not parsed.scheme: raise ValidationError( message=f"{field_name} must include a scheme (http:// or https://)", field_name=field_name, validation_rule="url_missing_scheme" ) allowed_schemes = allowed_schemes or ['http', 'https'] if parsed.scheme not in allowed_schemes: raise ValidationError( message=f"{field_name} scheme must be one of: {', '.join(allowed_schemes)}", field_name=field_name, validation_rule="url_invalid_scheme" ) if require_https and parsed.scheme != 'https': raise ValidationError( message=f"{field_name} must use HTTPS", field_name=field_name, validation_rule="url_https_required" ) # Check hostname if not parsed.netloc: raise ValidationError( message=f"{field_name} must include a hostname", field_name=field_name, validation_rule="url_missing_hostname" ) # Security checks for local/private URLs hostname = parsed.hostname if hostname: # Check for localhost/private IPs (warn but don't reject for development) if hostname in ['localhost', '127.0.0.1', '::1']: self.logger.info(f"Local URL detected in {field_name}: {validated_url}") elif hostname.startswith('192.168.') or hostname.startswith('10.') or hostname.startswith('172.'): self.logger.info(f"Private network URL detected in {field_name}: {validated_url}") return validated_url # Global validator instances input_validator = InputValidator() content_validator = ContentValidator() def validate_user_input( data: Dict[str, Any], validation_rules: Dict[str, Dict[str, Any]] ) -> Dict[str, Any]: """ Validate user input data against validation rules Args: data: Input data to validate validation_rules: Dictionary of field validation rules Returns: Dictionary of validated data Raises: ValidationError: If any validation fails """ validated_data = {} for field_name, rules in validation_rules.items(): value = data.get(field_name) # Get validation type and parameters validation_type = rules.get('type', 'string') required = rules.get('required', True) # Handle missing values if value is None or (isinstance(value, str) and not value.strip()): if required: raise ValidationError( message=f"{field_name} is required", field_name=field_name, validation_rule="required" ) else: validated_data[field_name] = None continue # Apply validation based on type try: if validation_type == 'string': validated_data[field_name] = input_validator.validate_string( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'integer': validated_data[field_name] = input_validator.validate_integer( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'float': validated_data[field_name] = input_validator.validate_float( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'boolean': validated_data[field_name] = input_validator.validate_boolean( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'list': validated_data[field_name] = input_validator.validate_list( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'dict': validated_data[field_name] = input_validator.validate_dict( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'json': validated_data[field_name] = input_validator.validate_json( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'url': validated_data[field_name] = content_validator.validate_url( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) elif validation_type == 'api_key': api_type = rules.get('api_type', 'generic') validated_data[field_name] = content_validator.validate_api_key( value, api_type, field_name ) elif validation_type == 'journal_content': validated_data[field_name] = content_validator.validate_journal_content( value, field_name, **{k: v for k, v in rules.items() if k not in ['type', 'required']} ) else: raise ValidationError( message=f"Unknown validation type: {validation_type}", field_name=field_name, validation_rule="unknown_validation_type" ) except ValidationError: # Re-raise validation errors as-is raise except Exception as e: # Wrap unexpected errors raise ValidationError( message=f"Validation error for {field_name}: {str(e)}", field_name=field_name, validation_rule="validation_error" ) return validated_data class CommandInputValidator: """Specialized validator for command inputs""" def __init__(self): self.logger = logging.getLogger(__name__) self.base_validator = InputValidator() self.content_validator = ContentValidator() def validate_organize_command_input(self, args: Dict[str, Any]) -> Dict[str, Any]: """ Validate input for the organize command Args: args: Command arguments to validate Returns: Validated arguments Raises: ValidationError: If validation fails """ validation_rules = { 'date': { 'type': 'string', 'required': False, 'pattern': 'date_iso', 'allow_empty': True }, 'vault_path': { 'type': 'string', 'required': False, 'min_length': 1, 'max_length': 4096, 'allow_empty': True }, 'daily_folder': { 'type': 'string', 'required': False, 'pattern': 'folder_name', 'min_length': 1, 'max_length': 255, 'allow_empty': True } } validated_args = validate_user_input(args, validation_rules) # Additional date validation if validated_args.get('date'): try: datetime.strptime(validated_args['date'], '%Y-%m-%d') except ValueError: raise ValidationError( message="Date must be in YYYY-MM-DD format", field_name="date", validation_rule="date_format" ) return validated_args def validate_skill_input(self, skill_name: str, kwargs: Dict[str, Any]) -> Dict[str, Any]: """ Validate input for specific skills Args: skill_name: Name of the skill kwargs: Skill arguments to validate Returns: Validated arguments Raises: ValidationError: If validation fails """ if skill_name == 'obsidian_read': return self._validate_obsidian_read_input(kwargs) elif skill_name == 'obsidian_write': return self._validate_obsidian_write_input(kwargs) elif skill_name == 'obsidian_append': return self._validate_obsidian_append_input(kwargs) elif skill_name == 'obsidian_list_files': return self._validate_obsidian_list_input(kwargs) elif skill_name == 'claude_analyze': return self._validate_claude_analyze_input(kwargs) elif skill_name == 'claude_transform': return self._validate_claude_transform_input(kwargs) else: # Generic validation for unknown skills return self._validate_generic_skill_input(kwargs) def _validate_obsidian_read_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Validate Obsidian read skill input""" validation_rules = { 'file_path': { 'type': 'string', 'required': True, 'min_length': 1, 'max_length': 4096 }, 'api_url': { 'type': 'url', 'required': False, 'allowed_schemes': ['http', 'https'] }, 'api_key': { 'type': 'api_key', 'required': True, 'api_type': 'obsidian' } } validated = validate_user_input(kwargs, validation_rules) # Additional file path validation if validated.get('file_path'): validated['file_path'] = self._sanitize_file_path(validated['file_path']) return validated def _validate_obsidian_write_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Validate Obsidian write skill input""" validation_rules = { 'file_path': { 'type': 'string', 'required': True, 'min_length': 1, 'max_length': 4096 }, 'content': { 'type': 'string', 'required': True, 'min_length': 0, # Allow empty content for creating empty files 'max_length': 1000000 # 1MB limit }, 'overwrite': { 'type': 'boolean', 'required': False }, 'api_url': { 'type': 'url', 'required': False, 'allowed_schemes': ['http', 'https'] }, 'api_key': { 'type': 'api_key', 'required': True, 'api_type': 'obsidian' } } validated = validate_user_input(kwargs, validation_rules) # Additional file path validation if validated.get('file_path'): validated['file_path'] = self._sanitize_file_path(validated['file_path']) return validated def _validate_obsidian_append_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Validate Obsidian append skill input""" validation_rules = { 'file_path': { 'type': 'string', 'required': True, 'min_length': 1, 'max_length': 4096 }, 'content': { 'type': 'string', 'required': True, 'min_length': 1, 'max_length': 1000000 # 1MB limit }, 'api_url': { 'type': 'url', 'required': False, 'allowed_schemes': ['http', 'https'] }, 'api_key': { 'type': 'api_key', 'required': True, 'api_type': 'obsidian' } } validated = validate_user_input(kwargs, validation_rules) # Additional file path validation if validated.get('file_path'): validated['file_path'] = self._sanitize_file_path(validated['file_path']) return validated def _validate_obsidian_list_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Validate Obsidian list files skill input""" validation_rules = { 'folder_path': { 'type': 'string', 'required': False, 'min_length': 0, 'max_length': 4096, 'allow_empty': True }, 'api_url': { 'type': 'url', 'required': False, 'allowed_schemes': ['http', 'https'] }, 'api_key': { 'type': 'api_key', 'required': True, 'api_type': 'obsidian' } } validated = validate_user_input(kwargs, validation_rules) # Additional folder path validation if validated.get('folder_path'): validated['folder_path'] = self._sanitize_file_path(validated['folder_path']) return validated def _validate_claude_analyze_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Validate Claude analyze skill input""" validation_rules = { 'journal_content': { 'type': 'journal_content', 'required': True }, 'api_key': { 'type': 'api_key', 'required': True, 'api_type': 'claude' }, 'model': { 'type': 'string', 'required': False, 'min_length': 1, 'max_length': 100 }, 'categories': { 'type': 'list', 'required': False, 'min_length': 0, 'max_length': 50 } } validated = validate_user_input(kwargs, validation_rules) # Validate model name if provided if validated.get('model'): valid_models = [ 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307' ] if validated['model'] not in valid_models: self.logger.warning(f"Unknown Claude model: {validated['model']}") # Validate categories if provided if validated.get('categories'): category_validator = lambda cat: self.base_validator.validate_string( cat, 'category', min_length=1, max_length=100 ) validated['categories'] = self.base_validator.validate_list( validated['categories'], 'categories', item_validator=category_validator ) return validated def _validate_claude_transform_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Validate Claude transform skill input""" validation_rules = { 'content': { 'type': 'string', 'required': True, 'min_length': 1, 'max_length': 1000000 # 1MB limit }, 'transform_type': { 'type': 'string', 'required': False, 'min_length': 1, 'max_length': 50 }, 'api_key': { 'type': 'api_key', 'required': True, 'api_type': 'claude' }, 'model': { 'type': 'string', 'required': False, 'min_length': 1, 'max_length': 100 } } validated = validate_user_input(kwargs, validation_rules) # Validate transform type if validated.get('transform_type'): valid_types = ['markdown', 'html', 'summary', 'outline', 'checklist'] if validated['transform_type'] not in valid_types: self.logger.warning(f"Unknown transform type: {validated['transform_type']}") return validated def _validate_generic_skill_input(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: """Generic validation for unknown skills""" validated = {} for key, value in kwargs.items(): if isinstance(value, str): # Basic string validation validated[key] = self.base_validator.validate_string( value, key, max_length=10000, allow_empty=True ) elif isinstance(value, (int, float)): # Keep numeric values as-is but validate range if isinstance(value, int): validated[key] = self.base_validator.validate_integer( value, key, min_value=-2147483648, max_value=2147483647 ) else: validated[key] = self.base_validator.validate_float( value, key, min_value=-1e10, max_value=1e10 ) elif isinstance(value, bool): validated[key] = value elif isinstance(value, (list, dict)): # Basic structure validation try: json.dumps(value) # Ensure JSON serializable validated[key] = value except (TypeError, ValueError): raise ValidationError( message=f"{key} contains non-serializable data", field_name=key, validation_rule="json_serializable" ) else: # Convert other types to string validated[key] = str(value) return validated def _sanitize_file_path(self, file_path: str) -> str: """ Sanitize file path to prevent directory traversal Args: file_path: File path to sanitize Returns: Sanitized file path Raises: ValidationError: If path is unsafe """ # Remove any null bytes file_path = file_path.replace('\x00', '') # Normalize path separators file_path = file_path.replace('\\', '/') # Check for directory traversal attempts if '..' in file_path: raise ValidationError( message="File path cannot contain '..' (directory traversal)", field_name="file_path", validation_rule="path_traversal" ) # Check for absolute paths (should be relative to vault) if file_path.startswith('/'): raise ValidationError( message="File path must be relative to vault root", field_name="file_path", validation_rule="absolute_path" ) # Check for dangerous characters dangerous_chars = ['<', '>', ':', '"', '|', '?', '*'] for char in dangerous_chars: if char in file_path: raise ValidationError( message=f"File path cannot contain '{char}'", field_name="file_path", validation_rule="dangerous_character" ) # Ensure path doesn't start with special directories path_parts = file_path.split('/') if path_parts and path_parts[0].startswith('.'): if path_parts[0] not in ['.obsidian']: # Allow .obsidian folder raise ValidationError( message="File path cannot start with hidden directory", field_name="file_path", validation_rule="hidden_directory" ) return file_path # Global command input validator instance command_input_validator = CommandInputValidator()