""" Input Validation and Security Module Handles prompt injection detection and input validation """ import re from typing import Tuple from config import HARMFUL_KEYWORDS, MAX_INPUT_LENGTH class InputValidator: """Validates user inputs for security and quality.""" @staticmethod def validate_input(text: str) -> Tuple[bool, str]: """ Validate user input for security and quality. Args: text: Input text to validate Returns: Tuple of (is_valid, error_message) """ # Check if empty if not text or not text.strip(): return False, "Input cannot be empty" # Check length if len(text) > MAX_INPUT_LENGTH: return False, f"Input too long (max {MAX_INPUT_LENGTH} characters)" # Check for prompt injection is_safe, msg = InputValidator.detect_prompt_injection(text) if not is_safe: return False, msg # Check for minimum meaningful content if len(text.strip().split()) < 3: return False, "Input too short. Please provide more context" return True, "Valid input" @staticmethod def detect_prompt_injection(text: str) -> Tuple[bool, str]: """ Detect potential prompt injection attacks. Uses blacklist of harmful keywords and patterns. Args: text: Text to check for prompt injection Returns: Tuple of (is_safe, warning_message) """ text_lower = text.lower() # Check for harmful keywords for keyword in HARMFUL_KEYWORDS: if keyword.lower() in text_lower: return False, f"⚠️ Blocked: Detected suspicious pattern: '{keyword}'" # Check for common injection patterns injection_patterns = [ r'```[\s\S]*?```', # Code blocks (potential instruction override) r'', # HTML comments r'\[SYSTEM\]', # System tokens r'\[IGNORE\]', # Ignore directives r'\\x[0-9a-f]{2}', # Hex encoding attempts ] for pattern in injection_patterns: if re.search(pattern, text, re.IGNORECASE): return False, "⚠️ Blocked: Detected suspicious instruction pattern" return True, "Safe input" @staticmethod def sanitize_input(text: str) -> str: """ Sanitize input by removing potentially harmful characters. Args: text: Text to sanitize Returns: Sanitized text """ # Remove null bytes text = text.replace('\x00', '') # Remove control characters except newlines and tabs text = ''.join( char for char in text if ord(char) >= 32 or char in '\n\t' ) # Remove multiple consecutive newlines text = re.sub(r'\n\n+', '\n\n', text) return text.strip() @staticmethod def validate_file_path(file_path: str) -> Tuple[bool, str]: """ Validate file path for security. Args: file_path: File path to validate Returns: Tuple of (is_valid, error_message) """ if not file_path: return False, "File path cannot be empty" # Check for path traversal attempts if ".." in file_path: return False, "Invalid file path: path traversal detected" # Check if path is absolute and outside project import os if os.path.isabs(file_path): project_root = os.path.dirname(os.path.abspath(__file__)) if not os.path.abspath(file_path).startswith(project_root): return False, "Invalid file path: outside project directory" return True, "Valid file path" class ContentValidator: """Validates content quality and relevance.""" @staticmethod def is_meaningful_response(text: str, min_words: int = 10) -> bool: """ Check if response is meaningful. Args: text: Text to validate min_words: Minimum words required Returns: True if meaningful, False otherwise """ words = text.strip().split() return len(words) >= min_words @staticmethod def estimate_quality(text: str) -> float: """ Estimate quality of a response (0.0 to 1.0). Args: text: Text to assess Returns: Quality score """ score = 0.0 # Length factor (max 0.3) word_count = len(text.split()) length_score = min(word_count / 100, 1.0) * 0.3 score += length_score # Structure factor (0.3) - presence of punctuation punctuation_count = sum(1 for c in text if c in '.!?;:') structure_score = min(punctuation_count / 10, 1.0) * 0.3 score += structure_score # Diversity factor (0.4) - lexical diversity words = text.lower().split() unique_words = len(set(words)) if len(words) > 0: diversity_score = (unique_words / len(words)) * 0.4 score += diversity_score return min(score, 1.0) @staticmethod def is_acceptable_summary(text: str, min_chars: int = 80) -> bool: """ Validate summary quality with flexible structured-output support. Args: text: Summary text min_chars: Minimum character threshold Returns: True when summary is acceptable """ if not text: return False cleaned = text.strip() if len(cleaned) >= min_chars: return True lower = cleaned.lower() required_sections = [ "key points", "main concept", "important details", "conclusion", ] has_all_sections = all(section in lower for section in required_sections) # Structured summaries can be concise but still useful. if has_all_sections and len(cleaned.split()) >= 20: return True return False class PromptValidator: """Validates and refines prompts for consistency.""" @staticmethod def validate_prompt_mode(mode: str) -> Tuple[bool, str]: """ Validate if prompt mode is supported. Args: mode: Mode name Returns: Tuple of (is_valid, message) """ valid_modes = ["normal", "detailed", "teacher", "exam"] if mode.lower() in valid_modes: return True, f"Valid mode: {mode}" else: return False, f"Invalid mode: {mode}. Choose from: {', '.join(valid_modes)}" @staticmethod def validate_feature(feature: str) -> Tuple[bool, str]: """ Validate if feature is supported. Args: feature: Feature name Returns: Tuple of (is_valid, message) """ valid_features = ["summarizer", "quiz", "explainer", "doubt_solver"] if feature.lower() in valid_features: return True, f"Valid feature: {feature}" else: return False, f"Invalid feature: {feature}. Choose from: {', '.join(valid_features)}"