Validates and sanitizes user input before it reaches the prompt builder. Three checks, in order: 1. Empty / whitespace-only input. 2. Character length exceeds budget. 3. Known injection pattern match (20 patterns, tested). Returns a ValidationResult. Never raises.
| 233 | |
| 234 | |
| 235 | class InputGuard: |
| 236 | """ |
| 237 | Validates and sanitizes user input before it reaches the prompt builder. |
| 238 | |
| 239 | Three checks, in order: |
| 240 | 1. Empty / whitespace-only input. |
| 241 | 2. Character length exceeds budget. |
| 242 | 3. Known injection pattern match (20 patterns, tested). |
| 243 | |
| 244 | Returns a ValidationResult. Never raises. |
| 245 | """ |
| 246 | |
| 247 | def __init__(self, max_input_chars: int = 2000): |
| 248 | self.max_input_chars = max_input_chars |
| 249 | self._patterns = [ |
| 250 | re.compile(p, re.IGNORECASE | re.DOTALL) |
| 251 | for p in INJECTION_PATTERNS |
| 252 | ] |
| 253 | |
| 254 | def validate(self, user_input: str) -> ValidationResult: |
| 255 | if not user_input or not user_input.strip(): |
| 256 | return ValidationResult( |
| 257 | passed=False, |
| 258 | failure_mode=FailureMode.CONSTRAINT_VIOLATION, |
| 259 | message="Input is empty.", |
| 260 | score=0.0, |
| 261 | ) |
| 262 | |
| 263 | if len(user_input) > self.max_input_chars: |
| 264 | return ValidationResult( |
| 265 | passed=False, |
| 266 | failure_mode=FailureMode.TOKEN_OVERFLOW, |
| 267 | message=( |
| 268 | f"Input exceeds {self.max_input_chars} chars " |
| 269 | f"({len(user_input)} received)." |
| 270 | ), |
| 271 | score=0.0, |
| 272 | ) |
| 273 | |
| 274 | for pattern in self._patterns: |
| 275 | if pattern.search(user_input): |
| 276 | return ValidationResult( |
| 277 | passed=False, |
| 278 | failure_mode=FailureMode.PROMPT_INJECTION, |
| 279 | message=f"Injection pattern detected: '{pattern.pattern[:60]}'", |
| 280 | score=0.0, |
| 281 | ) |
| 282 | |
| 283 | return ValidationResult(passed=True, score=1.0) |
| 284 | |
| 285 | def sanitize(self, user_input: str) -> str: |
| 286 | text = user_input.strip() |
| 287 | text = re.sub(r"\s+", " ", text) |
| 288 | # Strip null bytes and non-printable control chars (except newline/tab) |
| 289 | text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text) |
| 290 | return text |
| 291 | |
| 292 |
no outgoing calls