tcrm_task/tasks/validator.rs
1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::tasks::error::TaskError;
5const MAX_COMMAND_LEN: usize = 4096;
6const MAX_ARG_LEN: usize = 4096;
7const MAX_WORKING_DIR_LEN: usize = 4096;
8const MAX_ENV_KEY_LEN: usize = 1024;
9const MAX_ENV_VALUE_LEN: usize = 4096;
10/// Security validation utilities for task configuration
11pub struct ConfigValidator;
12
13impl ConfigValidator {
14 /// Validates command name.
15 ///
16 /// # Arguments
17 ///
18 /// * `command` - The command string to validate.
19 ///
20 /// # Returns
21 ///
22 /// - `Ok(())` if the command is valid.
23 /// - `Err(TaskError::InvalidConfiguration)` if the command is invalid.
24 ///
25 /// # Errors
26 ///
27 /// Returns a [`TaskError::InvalidConfiguration`] if:
28 /// - Command is empty or contains only whitespace
29 /// - Command has leading or trailing whitespace
30 /// - Command length exceeds maximum allowed length
31 ///
32 /// # Examples
33 /// ```rust
34 /// use tcrm_task::tasks::validator::ConfigValidator;
35 ///
36 /// let valid_command = "echo";
37 /// assert!(ConfigValidator::validate_command(valid_command).is_ok());
38 ///
39 /// let invalid_command = "";
40 /// assert!(ConfigValidator::validate_command(invalid_command).is_err());
41 /// ```
42 pub fn validate_command(command: &str) -> Result<(), TaskError> {
43 // Check for empty or whitespace-only commands
44 if command.trim().is_empty() {
45 return Err(TaskError::InvalidConfiguration(
46 "Command cannot be empty".to_string(),
47 ));
48 }
49
50 // Only check for obvious injection attempts, not shell features
51 // if Self::contains_obvious_injection(command) {
52 // return Err(TaskError::InvalidConfiguration(
53 // "Command contains potentially dangerous injection patterns".to_string(),
54 // ));
55 // }
56
57 if command.trim() != command {
58 return Err(TaskError::InvalidConfiguration(
59 "Command cannot have leading or trailing whitespace".to_string(),
60 ));
61 }
62 if command.len() > MAX_COMMAND_LEN {
63 return Err(TaskError::InvalidConfiguration(
64 "Command length exceeds maximum allowed length".to_string(),
65 ));
66 }
67
68 Ok(())
69 }
70
71 /// Validates arguments.
72 ///
73 /// # Arguments
74 ///
75 /// * `args` - A slice of argument strings to validate.
76 ///
77 /// # Returns
78 ///
79 /// - `Ok(())` if all arguments are valid.
80 /// - `Err(TaskError::InvalidConfiguration)` if any argument is invalid.
81 ///
82 /// # Errors
83 ///
84 /// Returns a [`TaskError::InvalidConfiguration`] if:
85 /// - any argument contains null bytes
86 /// - any argument is an empty string
87 /// - any argument has leading or trailing whitespace
88 /// - any argument exceeds maximum length
89 ///
90 /// # Examples
91 /// ```rust
92 /// use tcrm_task::tasks::validator::ConfigValidator;
93 ///
94 /// let valid_args = vec!["arg1".to_string(), "arg2".to_string()];
95 /// assert!(ConfigValidator::validate_args(&valid_args).is_ok());
96 ///
97 /// let invalid_args = vec!["arg1".to_string(), "\0".to_string()];
98 /// assert!(ConfigValidator::validate_args(&invalid_args).is_err());
99 /// ```
100 pub fn validate_args(args: &[String]) -> Result<(), TaskError> {
101 for arg in args {
102 // Only check for null bytes
103 if arg.contains('\0') {
104 return Err(TaskError::InvalidConfiguration(
105 "Argument contains null characters".to_string(),
106 ));
107 }
108 if arg.is_empty() {
109 return Err(TaskError::InvalidConfiguration(
110 "Arguments cannot be empty".to_string(),
111 ));
112 }
113 if arg.trim() != arg {
114 return Err(TaskError::InvalidConfiguration(format!(
115 "Argument '{arg}' cannot have leading/trailing whitespace"
116 )));
117 }
118 if arg.len() > MAX_ARG_LEN {
119 return Err(TaskError::InvalidConfiguration(format!(
120 "Argument '{arg}' exceeds maximum length"
121 )));
122 }
123 }
124 Ok(())
125 }
126
127 /// Validates the working directory path.
128 ///
129 /// # Arguments
130 ///
131 /// * `dir` - The directory path to validate.
132 ///
133 /// # Returns
134 ///
135 /// - `Ok(())` if the directory is valid.
136 /// - `Err(TaskError::InvalidConfiguration)` if the directory is invalid.
137 ///
138 /// # Errors
139 ///
140 /// Returns a [`TaskError::InvalidConfiguration`] if:
141 /// - Directory does not exist
142 /// - Path exists but is not a directory
143 /// - Directory has leading or trailing whitespace
144 /// - Directory path exceeds maximum length
145 ///
146 /// # Examples
147 /// ```rust
148 /// use tcrm_task::tasks::validator::ConfigValidator;
149 /// use std::env;
150 ///
151 /// // Test with current directory (should exist)
152 /// let current_dir = env::current_dir().unwrap();
153 /// let valid_dir = current_dir.to_str().unwrap();
154 /// assert!(ConfigValidator::validate_working_dir(valid_dir).is_ok());
155 ///
156 /// // Test with nonexistent directory
157 /// let invalid_dir = "nonexistent_dir_12345";
158 /// assert!(ConfigValidator::validate_working_dir(invalid_dir).is_err());
159 /// ```
160 pub fn validate_working_dir(dir: &str) -> Result<(), TaskError> {
161 let path = Path::new(dir);
162
163 // Check if path exists
164 if !path.exists() {
165 return Err(TaskError::IO(format!(
166 "Working directory does not exist: {dir}"
167 )));
168 }
169
170 // Check if it's actually a directory
171 if !path.is_dir() {
172 return Err(TaskError::InvalidConfiguration(format!(
173 "Working directory is not a directory: {dir}"
174 )));
175 }
176
177 if dir.trim() != dir {
178 return Err(TaskError::InvalidConfiguration(
179 "Working directory cannot have leading/trailing whitespace".to_string(),
180 ));
181 }
182 if dir.len() > MAX_WORKING_DIR_LEN {
183 return Err(TaskError::InvalidConfiguration(
184 "Working directory path exceeds maximum length".to_string(),
185 ));
186 }
187
188 Ok(())
189 }
190
191 /// Validates environment variables.
192 ///
193 /// # Arguments
194 ///
195 /// * `env` - A hashmap of environment variable key-value pairs to validate.
196 ///
197 /// # Returns
198 ///
199 /// - `Ok(())` if all environment variables are valid.
200 /// - `Err(TaskError::InvalidConfiguration)` if any environment variable is invalid.
201 ///
202 /// # Errors
203 ///
204 /// Returns a [`TaskError::InvalidConfiguration`] if:
205 /// - Any environment variable key contains spaces, '=', or null bytes
206 /// - Any environment variable value contains null bytes
207 /// - Any environment variable key/value exceeds maximum length
208 ///
209 /// # Examples
210 /// ```rust
211 /// use tcrm_task::tasks::validator::ConfigValidator;
212 /// use std::collections::HashMap;
213 ///
214 /// let mut valid_env = HashMap::new();
215 /// valid_env.insert("KEY".to_string(), "VALUE".to_string());
216 /// assert!(ConfigValidator::validate_env_vars(&valid_env).is_ok());
217 ///
218 /// let mut invalid_env = HashMap::new();
219 /// invalid_env.insert("KEY\0".to_string(), "VALUE".to_string());
220 /// assert!(ConfigValidator::validate_env_vars(&invalid_env).is_err());
221 /// ```
222 pub fn validate_env_vars(env: &HashMap<String, String>) -> Result<(), TaskError> {
223 for (key, value) in env {
224 // Validate key
225 if key.trim().is_empty() {
226 return Err(TaskError::InvalidConfiguration(
227 "Environment variable key cannot be empty".to_string(),
228 ));
229 }
230 if key.contains('=') || key.contains('\0') || key.contains('\t') || key.contains('\n') {
231 return Err(TaskError::InvalidConfiguration(
232 "Environment variable key contains invalid characters".to_string(),
233 ));
234 }
235
236 if key.contains(' ') {
237 return Err(TaskError::InvalidConfiguration(format!(
238 "Environment variable key '{key}' cannot contain spaces"
239 )));
240 }
241
242 if key.len() > MAX_ENV_KEY_LEN {
243 return Err(TaskError::InvalidConfiguration(format!(
244 "Environment variable key '{key}' exceeds maximum length"
245 )));
246 }
247
248 // Validate value
249 if value.contains('\0') {
250 return Err(TaskError::InvalidConfiguration(
251 "Environment variable value contains null characters".to_string(),
252 ));
253 }
254
255 if value.trim() != value {
256 return Err(TaskError::InvalidConfiguration(format!(
257 "Environment variable '{key}' value cannot have leading/trailing whitespace"
258 )));
259 }
260 if value.len() > MAX_ENV_VALUE_LEN {
261 return Err(TaskError::InvalidConfiguration(format!(
262 "Environment variable '{key}' value exceeds maximum length"
263 )));
264 }
265 }
266 Ok(())
267 }
268
269 /// Validates ready indicator string
270 ///
271 /// # Arguments
272 ///
273 /// * `indicator` - The ready indicator string to validate
274 ///
275 /// # Returns
276 ///
277 /// - `Ok(())` if the indicator is valid
278 /// - `Err(TaskError::InvalidConfiguration)` if the indicator is invalid
279 ///
280 /// # Errors
281 ///
282 /// Returns [`TaskError::InvalidConfiguration`] if:
283 /// - Indicator is empty
284 ///
285 /// # Examples
286 ///
287 /// ```rust
288 /// use tcrm_task::tasks::validator::ConfigValidator;
289 ///
290 /// let valid_indicator = "ready";
291 /// assert!(ConfigValidator::validate_ready_indicator(valid_indicator).is_ok());
292 ///
293 /// let invalid_indicator = "";
294 /// assert!(ConfigValidator::validate_ready_indicator(invalid_indicator).is_err());
295 /// ```
296 pub fn validate_ready_indicator(indicator: &str) -> Result<(), TaskError> {
297 if indicator.is_empty() {
298 return Err(TaskError::InvalidConfiguration(
299 "ready_indicator cannot be empty string".to_string(),
300 ));
301 }
302 Ok(())
303 }
304
305 /// Validates timeout value.
306 ///
307 /// Ensures that the timeout value is greater than 0. A timeout of 0 would mean
308 /// immediate timeout, which is not useful for task execution.
309 ///
310 /// # Arguments
311 ///
312 /// * `timeout` - The timeout value in milliseconds to validate
313 ///
314 /// # Returns
315 ///
316 /// * `Ok(())` - If timeout is valid (greater than 0)
317 /// * `Err(TaskError)` - If timeout is 0
318 ///
319 /// # Example
320 ///
321 /// ```rust
322 /// use tcrm_task::tasks::validator::ConfigValidator;
323 ///
324 /// // Valid timeout
325 /// assert!(ConfigValidator::validate_timeout(&5000).is_ok());
326 ///
327 /// // Invalid timeout (0)
328 /// assert!(ConfigValidator::validate_timeout(&0).is_err());
329 /// ```
330 pub fn validate_timeout(timeout: &u64) -> Result<(), TaskError> {
331 if *timeout == 0 {
332 return Err(TaskError::InvalidConfiguration(
333 "Timeout must be greater than 0".to_string(),
334 ));
335 }
336 Ok(())
337 }
338
339 /// Checks for obvious injection attempts while allowing normal shell features.
340 ///
341 /// This method identifies clearly malicious patterns without blocking
342 /// legitimate shell functionality. It focuses on patterns that are rarely used
343 /// in normal command execution.
344 ///
345 /// # Arguments
346 ///
347 /// * `input` - The command string to check for injection patterns.
348 ///
349 /// # Returns
350 ///
351 /// `true` if obvious injection patterns are detected, `false` otherwise.
352 pub fn contains_obvious_injection(input: &str) -> bool {
353 // Only block patterns that are clearly malicious, not functional shell features
354 let obvious_injection_patterns = [
355 "\0", // Null bytes
356 "\x00", // Null bytes (hex)
357 "\r\n", // CRLF injection
358 "eval(", // Direct eval calls
359 "exec(", // Direct exec calls
360 "os.system(", // Direct Python code execution
361 ];
362
363 obvious_injection_patterns
364 .iter()
365 .any(|pattern| input.contains(pattern))
366 }
367
368 /// Validates command with strict security rules for untrusted input sources.
369 ///
370 /// This is an alternative to `validate_command` that blocks all shell features
371 ///
372 /// # Arguments
373 ///
374 /// * `command` - The command string to validate strictly.
375 ///
376 /// # Returns
377 ///
378 /// - `Ok(())` if the command passes strict validation.
379 /// - `Err(TaskError::InvalidConfiguration)` if the command contains potentially dangerous patterns.
380 ///
381 /// # Errors
382 ///
383 /// Returns a [`TaskError::InvalidConfiguration`] if:
384 /// - Command is empty or contains only whitespace
385 /// - Command contains shell metacharacters or redirection operators
386 ///
387 /// # Examples
388 /// ```rust
389 /// use tcrm_task::tasks::validator::ConfigValidator;
390 ///
391 /// // Simple command should pass
392 /// let simple_command = "echo";
393 /// assert!(ConfigValidator::validate_command_strict(simple_command).is_ok());
394 ///
395 /// // Command with shell features should fail
396 /// let shell_command = "echo hello; rm -rf /";
397 /// assert!(ConfigValidator::validate_command_strict(shell_command).is_err());
398 /// ```
399 #[allow(dead_code)]
400 pub fn validate_command_strict(command: &str) -> Result<(), TaskError> {
401 // Check for empty or whitespace-only commands
402 if command.trim().is_empty() {
403 return Err(TaskError::InvalidConfiguration(
404 "Command cannot be empty".to_string(),
405 ));
406 }
407
408 // Strict validation - blocks shell features
409 let dangerous_patterns = [
410 ";", "&", "|", "`", "#", "$", "(", ")", "{", "}", "[", "]", "<", ">", "&&", "||", ">>",
411 "<<", "\n", "\r",
412 ];
413
414 if dangerous_patterns
415 .iter()
416 .any(|pattern| command.contains(pattern))
417 {
418 return Err(TaskError::InvalidConfiguration(
419 "Command contains shell metacharacters (use validate_command for developer tools)"
420 .to_string(),
421 ));
422 }
423
424 Ok(())
425 }
426}