ggen_core/security/
validation.rs1use ggen_utils::error::{Error, Result};
9use std::path::{Path, PathBuf};
10
11#[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
39pub struct PathValidator;
41
42impl PathValidator {
43 const DANGEROUS_COMPONENTS: &'static [&'static str] = &[
45 "..", "~", "$", "`", "|", ";", "&", "<", ">", "(", ")", "{", "}", "\n", "\r",
46 ];
47
48 const MAX_PATH_LENGTH: usize = 4096;
50
51 pub fn validate(path: &Path) -> Result<PathBuf> {
76 let path_str = path.to_string_lossy();
77
78 if path_str.len() > Self::MAX_PATH_LENGTH {
80 return Err(ValidationError::TooLong(path_str.len(), Self::MAX_PATH_LENGTH).into());
81 }
82
83 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 Ok(path.to_path_buf())
101 }
102
103 pub fn validate_within(path: &Path, base: &Path) -> Result<PathBuf> {
110 let validated = Self::validate(path)?;
111
112 let abs_path = if validated.is_relative() {
114 base.join(&validated)
115 } else {
116 validated.clone()
117 };
118
119 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 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
151pub struct EnvVarValidator;
153
154impl EnvVarValidator {
155 const DANGEROUS_CHARS: &'static [char] = &[
157 ';', '|', '&', '$', '`', '\n', '\r', '<', '>', '(', ')', '{', '}', '\\',
158 ];
159
160 const MAX_ENV_LENGTH: usize = 32768;
162
163 pub fn validate_name(name: &str) -> Result<String> {
165 if name.is_empty() {
166 return Err(ValidationError::EmptyInput.into());
167 }
168
169 if name.len() > Self::MAX_ENV_LENGTH {
171 return Err(ValidationError::TooLong(name.len(), Self::MAX_ENV_LENGTH).into());
172 }
173
174 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 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 pub fn validate_value(value: &str) -> Result<String> {
197 if value.len() > Self::MAX_ENV_LENGTH {
199 return Err(ValidationError::TooLong(value.len(), Self::MAX_ENV_LENGTH).into());
200 }
201
202 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
214pub struct InputValidator;
216
217impl InputValidator {
218 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 pub fn validate_identifier(input: &str) -> Result<String> {
242 Self::validate_string(input, 256, |c| c.is_alphanumeric() || c == '_' || c == '-')
243 }
244
245 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 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 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 let long_path = "a/".repeat(3000);
273 assert!(PathValidator::validate(Path::new(&long_path)).is_err());
274
275 assert!(PathValidator::validate(Path::new("src/lib.rs")).is_ok());
277 }
278
279 #[test]
280 fn test_env_var_name_validation() {
281 assert!(EnvVarValidator::validate_name("PATH").is_ok());
283 assert!(EnvVarValidator::validate_name("MY_VAR_123").is_ok());
284
285 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 assert!(EnvVarValidator::validate_value("value").is_ok());
296 assert!(EnvVarValidator::validate_value("/usr/bin").is_ok());
297
298 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 assert!(InputValidator::validate_identifier("my_var").is_ok());
308 assert!(InputValidator::validate_identifier("my-var-123").is_ok());
309
310 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 assert!(InputValidator::validate_template_name("rust-cli").is_ok());
320 assert!(InputValidator::validate_template_name("templates/rust.tmpl").is_ok());
321
322 assert!(InputValidator::validate_template_name("").is_err());
324 assert!(InputValidator::validate_template_name("template; rm -rf /").is_err());
325 }
326}