ggen_core/security/
validation.rs

1//! Input validation to prevent injection and exploitation
2//!
3//! **SECURITY ISSUE 4: Input Validation**
4//!
5//! This module provides comprehensive validation for paths, environment variables,
6//! and user inputs to prevent exploitation via malformed or malicious data.
7
8use ggen_utils::error::{Error, Result};
9use std::path::{Path, PathBuf};
10
11/// Error type for validation failures
12#[derive(Debug, thiserror::Error)]
13pub enum ValidationError {
14    #[error("Invalid path: {0}")]
15    InvalidPath(String),
16
17    #[error("Path traversal detected: {0}")]
18    PathTraversal(String),
19
20    #[error("Invalid environment variable: {0}")]
21    InvalidEnvVar(String),
22
23    #[error("Input too long: {0} (max: {1})")]
24    TooLong(usize, usize),
25
26    #[error("Invalid characters: {0}")]
27    InvalidCharacters(String),
28
29    #[error("Empty input")]
30    EmptyInput,
31}
32
33impl From<ValidationError> for Error {
34    fn from(err: ValidationError) -> Self {
35        Error::new(&err.to_string())
36    }
37}
38
39/// Path validator to prevent traversal attacks
40pub struct PathValidator;
41
42impl PathValidator {
43    /// Dangerous path components
44    const DANGEROUS_COMPONENTS: &'static [&'static str] = &[
45        "..", "~", "$", "`", "|", ";", "&", "<", ">", "(", ")", "{", "}", "\n", "\r",
46    ];
47
48    /// Maximum path length
49    const MAX_PATH_LENGTH: usize = 4096;
50
51    /// Validate a path for safety
52    ///
53    /// # Security
54    ///
55    /// - Prevents `../../../etc/passwd` traversal attacks
56    /// - Rejects paths with dangerous characters
57    /// - Enforces maximum length limits
58    /// - Ensures path is within allowed bounds
59    ///
60    /// # Examples
61    ///
62    /// ```rust
63    /// use ggen_core::security::validation::PathValidator;
64    /// use std::path::Path;
65    ///
66    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
67    /// // Safe path
68    /// let path = PathValidator::validate(Path::new("src/main.rs"))?;
69    ///
70    /// // Unsafe path (path traversal)
71    /// assert!(PathValidator::validate(Path::new("../../../etc/passwd")).is_err());
72    /// # Ok(())
73    /// # }
74    /// ```
75    pub fn validate(path: &Path) -> Result<PathBuf> {
76        let path_str = path.to_string_lossy();
77
78        // Check length
79        if path_str.len() > Self::MAX_PATH_LENGTH {
80            return Err(ValidationError::TooLong(path_str.len(), Self::MAX_PATH_LENGTH).into());
81        }
82
83        // Check for dangerous components
84        for component in path.components() {
85            let component_str = component.as_os_str().to_string_lossy();
86
87            for dangerous in Self::DANGEROUS_COMPONENTS {
88                if component_str.contains(dangerous) {
89                    return Err(ValidationError::PathTraversal(format!(
90                        "Path contains dangerous component: {}",
91                        component_str
92                    ))
93                    .into());
94                }
95            }
96        }
97
98        // Canonicalize to resolve any remaining issues
99        // Note: This will fail if path doesn't exist, which is fine for validation
100        Ok(path.to_path_buf())
101    }
102
103    /// Validate path is within a base directory (no traversal outside)
104    ///
105    /// # Security
106    ///
107    /// - Ensures path stays within allowed directory tree
108    /// - Prevents escaping via symlinks or ../ sequences
109    pub fn validate_within(path: &Path, base: &Path) -> Result<PathBuf> {
110        let validated = Self::validate(path)?;
111
112        // Ensure path is within base
113        let abs_path = if validated.is_relative() {
114            base.join(&validated)
115        } else {
116            validated.clone()
117        };
118
119        // Check if path starts with base
120        if !abs_path.starts_with(base) {
121            return Err(ValidationError::PathTraversal(format!(
122                "Path escapes base directory: {} not in {}",
123                abs_path.display(),
124                base.display()
125            ))
126            .into());
127        }
128
129        Ok(validated)
130    }
131
132    /// Validate file extension is in allowed list
133    pub fn validate_extension(path: &Path, allowed: &[&str]) -> Result<()> {
134        let ext = path
135            .extension()
136            .and_then(|e| e.to_str())
137            .ok_or_else(|| ValidationError::InvalidPath("No file extension".to_string()))?;
138
139        if !allowed.contains(&ext) {
140            return Err(ValidationError::InvalidPath(format!(
141                "Extension '{}' not in allowed list",
142                ext
143            ))
144            .into());
145        }
146
147        Ok(())
148    }
149}
150
151/// Environment variable validator
152pub struct EnvVarValidator;
153
154impl EnvVarValidator {
155    /// Dangerous characters in environment variables
156    const DANGEROUS_CHARS: &'static [char] = &[
157        ';', '|', '&', '$', '`', '\n', '\r', '<', '>', '(', ')', '{', '}', '\\',
158    ];
159
160    /// Maximum environment variable length
161    const MAX_ENV_LENGTH: usize = 32768;
162
163    /// Validate environment variable name
164    pub fn validate_name(name: &str) -> Result<String> {
165        if name.is_empty() {
166            return Err(ValidationError::EmptyInput.into());
167        }
168
169        // Check length
170        if name.len() > Self::MAX_ENV_LENGTH {
171            return Err(ValidationError::TooLong(name.len(), Self::MAX_ENV_LENGTH).into());
172        }
173
174        // Check for dangerous characters
175        if name.chars().any(|c| Self::DANGEROUS_CHARS.contains(&c)) {
176            return Err(ValidationError::InvalidCharacters(format!(
177                "Environment variable name contains dangerous characters: {}",
178                name
179            ))
180            .into());
181        }
182
183        // Ensure alphanumeric + underscore only
184        if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
185            return Err(ValidationError::InvalidCharacters(format!(
186                "Environment variable name must be alphanumeric: {}",
187                name
188            ))
189            .into());
190        }
191
192        Ok(name.to_string())
193    }
194
195    /// Validate environment variable value
196    pub fn validate_value(value: &str) -> Result<String> {
197        // Check length
198        if value.len() > Self::MAX_ENV_LENGTH {
199            return Err(ValidationError::TooLong(value.len(), Self::MAX_ENV_LENGTH).into());
200        }
201
202        // Check for shell metacharacters that could be dangerous
203        if value.chars().any(|c| Self::DANGEROUS_CHARS.contains(&c)) {
204            return Err(ValidationError::InvalidCharacters(
205                "Environment variable value contains dangerous characters".to_string(),
206            )
207            .into());
208        }
209
210        Ok(value.to_string())
211    }
212}
213
214/// General input validator
215pub struct InputValidator;
216
217impl InputValidator {
218    /// Validate string input with length and character restrictions
219    pub fn validate_string(
220        input: &str, max_length: usize, allowed_chars: fn(char) -> bool,
221    ) -> Result<String> {
222        if input.is_empty() {
223            return Err(ValidationError::EmptyInput.into());
224        }
225
226        if input.len() > max_length {
227            return Err(ValidationError::TooLong(input.len(), max_length).into());
228        }
229
230        if !input.chars().all(allowed_chars) {
231            return Err(ValidationError::InvalidCharacters(
232                "Input contains invalid characters".to_string(),
233            )
234            .into());
235        }
236
237        Ok(input.to_string())
238    }
239
240    /// Validate identifier (alphanumeric + underscore/hyphen)
241    pub fn validate_identifier(input: &str) -> Result<String> {
242        Self::validate_string(input, 256, |c| c.is_alphanumeric() || c == '_' || c == '-')
243    }
244
245    /// Validate template name
246    pub fn validate_template_name(input: &str) -> Result<String> {
247        Self::validate_string(input, 256, |c| {
248            c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/'
249        })
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_path_traversal_detection() {
259        // Path traversal attempts
260        assert!(PathValidator::validate(Path::new("../../../etc/passwd")).is_err());
261        assert!(PathValidator::validate(Path::new("../../.ssh/id_rsa")).is_err());
262        assert!(PathValidator::validate(Path::new("..\\..\\windows\\system32")).is_err());
263
264        // Safe paths
265        assert!(PathValidator::validate(Path::new("src/main.rs")).is_ok());
266        assert!(PathValidator::validate(Path::new("templates/rust.tmpl")).is_ok());
267    }
268
269    #[test]
270    fn test_path_length_validation() {
271        // Too long path
272        let long_path = "a/".repeat(3000);
273        assert!(PathValidator::validate(Path::new(&long_path)).is_err());
274
275        // Normal path
276        assert!(PathValidator::validate(Path::new("src/lib.rs")).is_ok());
277    }
278
279    #[test]
280    fn test_env_var_name_validation() {
281        // Valid names
282        assert!(EnvVarValidator::validate_name("PATH").is_ok());
283        assert!(EnvVarValidator::validate_name("MY_VAR_123").is_ok());
284
285        // Invalid names
286        assert!(EnvVarValidator::validate_name("").is_err());
287        assert!(EnvVarValidator::validate_name("VAR; rm -rf /").is_err());
288        assert!(EnvVarValidator::validate_name("VAR|cat").is_err());
289        assert!(EnvVarValidator::validate_name("$(whoami)").is_err());
290    }
291
292    #[test]
293    fn test_env_var_value_validation() {
294        // Valid values
295        assert!(EnvVarValidator::validate_value("value").is_ok());
296        assert!(EnvVarValidator::validate_value("/usr/bin").is_ok());
297
298        // Invalid values (shell metacharacters)
299        assert!(EnvVarValidator::validate_value("value; rm -rf /").is_err());
300        assert!(EnvVarValidator::validate_value("$(whoami)").is_err());
301        assert!(EnvVarValidator::validate_value("`whoami`").is_err());
302    }
303
304    #[test]
305    fn test_identifier_validation() {
306        // Valid identifiers
307        assert!(InputValidator::validate_identifier("my_var").is_ok());
308        assert!(InputValidator::validate_identifier("my-var-123").is_ok());
309
310        // Invalid identifiers
311        assert!(InputValidator::validate_identifier("").is_err());
312        assert!(InputValidator::validate_identifier("my var").is_err());
313        assert!(InputValidator::validate_identifier("my;var").is_err());
314    }
315
316    #[test]
317    fn test_template_name_validation() {
318        // Valid template names
319        assert!(InputValidator::validate_template_name("rust-cli").is_ok());
320        assert!(InputValidator::validate_template_name("templates/rust.tmpl").is_ok());
321
322        // Invalid template names
323        assert!(InputValidator::validate_template_name("").is_err());
324        assert!(InputValidator::validate_template_name("template; rm -rf /").is_err());
325    }
326}