gamecode_mcp2/
validation.rs

1// Input validation for LLM-provided arguments
2// Minimal approach - document what we check and why
3
4use anyhow::{bail, Result};
5use serde_json::Value;
6
7// Validate file paths - prevent directory traversal
8pub fn validate_path(path: &str, allow_absolute: bool) -> Result<()> {
9    // Reject null bytes (filesystem terminator)
10    if path.contains('\0') {
11        bail!("Path contains null byte");
12    }
13    
14    // Reject path traversal patterns
15    if path.contains("..") {
16        bail!("Path traversal detected: '..' not allowed");
17    }
18    
19    // Optionally reject absolute paths
20    if !allow_absolute && (path.starts_with('/') || path.starts_with('~')) {
21        bail!("Absolute paths not allowed");
22    }
23    
24    Ok(())
25}
26
27// Validate command arguments for common injection patterns
28pub fn validate_command_arg(arg: &str) -> Result<()> {
29    // Reject null bytes
30    if arg.contains('\0') {
31        bail!("Argument contains null byte");
32    }
33    
34    // These would only be dangerous with shell interpretation,
35    // but checking them adds defense in depth
36    const SUSPICIOUS_PATTERNS: &[&str] = &[
37        "$(",      // Command substitution
38        "`",       // Backtick substitution  
39        "${",      // Variable expansion
40        "&&",      // Command chaining
41        "||",      // Command chaining
42        ";",       // Command separator
43        "|",       // Pipe
44        ">",       // Redirect
45        "<",       // Redirect
46        "\n",      // Newline (command separator)
47        "\r",      // Carriage return
48    ];
49    
50    for pattern in SUSPICIOUS_PATTERNS {
51        if arg.contains(pattern) {
52            // Log but don't reject - these are safe without shell
53            tracing::warn!("Suspicious pattern '{}' in argument: {}", pattern, arg);
54        }
55    }
56    
57    Ok(())
58}
59
60// Validate based on expected type
61pub fn validate_typed_value(value: &Value, expected_type: &str) -> Result<()> {
62    match (expected_type, value) {
63        ("string", Value::String(s)) => {
64            validate_command_arg(s)?;
65        }
66        ("number", Value::Number(_)) => {
67            // Numbers are generally safe
68        }
69        ("boolean", Value::Bool(_)) => {
70            // Booleans are safe
71        }
72        ("array", Value::Array(arr)) => {
73            // Validate each element
74            for item in arr {
75                if let Value::String(s) = item {
76                    validate_command_arg(s)?;
77                }
78            }
79        }
80        _ => {
81            bail!("Type mismatch: expected {}, got {:?}", expected_type, value);
82        }
83    }
84    Ok(())
85}
86
87// Rate limiting check (requires external state)
88#[allow(dead_code)]
89pub fn check_rate_limit(tool_name: &str, window_ms: u64) -> Result<()> {
90    // This would need to be implemented with a time-based counter
91    // For now, just document the interface
92    tracing::debug!("Rate limit check for {} ({}ms window)", tool_name, window_ms);
93    Ok(())
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_path_validation() {
102        // Should pass
103        assert!(validate_path("file.txt", false).is_ok());
104        assert!(validate_path("dir/file.txt", false).is_ok());
105        assert!(validate_path("/etc/passwd", true).is_ok());
106        
107        // Should fail
108        assert!(validate_path("../etc/passwd", false).is_err());
109        assert!(validate_path("/etc/passwd", false).is_err());
110        assert!(validate_path("~/ssh/config", false).is_err());
111        assert!(validate_path("file\0.txt", false).is_err());
112    }
113
114    #[test]
115    fn test_command_validation() {
116        // Should pass (but may log warnings)
117        assert!(validate_command_arg("hello world").is_ok());
118        assert!(validate_command_arg("--flag=value").is_ok());
119        
120        // Should pass but log warnings  
121        assert!(validate_command_arg("test; ls").is_ok());
122        assert!(validate_command_arg("$(whoami)").is_ok());
123        
124        // Should fail
125        assert!(validate_command_arg("test\0null").is_err());
126    }
127}