netwatch_rs/
validation.rs

1//! Input validation and sanitization for netwatch
2//!
3//! This module provides secure validation functions for all user inputs
4//! to prevent injection attacks, path traversal, and other security issues.
5
6use crate::error::{NetwatchError, Result};
7use crate::security::{record_security_event, SecurityEvent};
8use std::path::Path;
9
10/// Maximum allowed length for network interface names
11const MAX_INTERFACE_NAME_LEN: usize = 16;
12
13/// Maximum allowed length for file paths
14const MAX_PATH_LEN: usize = 4096;
15
16/// Maximum allowed refresh interval in milliseconds
17const MAX_REFRESH_INTERVAL: u64 = 60_000; // 1 minute
18
19/// Minimum allowed refresh interval in milliseconds
20const MIN_REFRESH_INTERVAL: u64 = 100; // 0.1 seconds
21
22/// Validates network interface names to prevent path traversal and injection
23///
24/// # Security Considerations
25/// - Prevents path traversal attacks (../../../etc/passwd)
26/// - Blocks null bytes and control characters
27/// - Limits length to prevent buffer overflow attacks
28/// - Only allows safe characters commonly used in interface names
29///
30/// # Examples
31/// ```
32/// use netwatch_rs::validation::validate_interface_name;
33///
34/// assert!(validate_interface_name("eth0").is_ok());
35/// assert!(validate_interface_name("wlan0").is_ok());
36/// assert!(validate_interface_name("../etc/passwd").is_err());
37/// ```
38pub fn validate_interface_name(name: &str) -> Result<()> {
39    // Check for empty or overly long names
40    if name.is_empty() {
41        record_security_event(SecurityEvent::InvalidInput {
42            input_type: "interface_name".to_string(),
43            attempted_value: name.to_string(),
44            source: "validation".to_string(),
45        });
46        return Err(NetwatchError::Parse(
47            "Interface name cannot be empty".to_string(),
48        ));
49    }
50
51    if name.len() > MAX_INTERFACE_NAME_LEN {
52        record_security_event(SecurityEvent::InvalidInput {
53            input_type: "interface_name".to_string(),
54            attempted_value: name.to_string(),
55            source: "validation".to_string(),
56        });
57        return Err(NetwatchError::Parse(format!(
58            "Interface name too long (max {MAX_INTERFACE_NAME_LEN} characters)"
59        )));
60    }
61
62    // Check for path traversal attempts
63    if name.contains("..") || name.contains('/') || name.contains('\\') {
64        record_security_event(SecurityEvent::InvalidInput {
65            input_type: "interface_name".to_string(),
66            attempted_value: name.to_string(),
67            source: "validation".to_string(),
68        });
69        return Err(NetwatchError::Parse(
70            "Invalid characters in interface name".to_string(),
71        ));
72    }
73
74    // Check for null bytes and control characters
75    if name.contains('\0') || name.chars().any(|c| c.is_control()) {
76        record_security_event(SecurityEvent::InvalidInput {
77            input_type: "interface_name".to_string(),
78            attempted_value: name.to_string(),
79            source: "validation".to_string(),
80        });
81        return Err(NetwatchError::Parse(
82            "Control characters not allowed in interface name".to_string(),
83        ));
84    }
85
86    // Only allow alphanumeric characters, hyphens, underscores, and dots
87    if !name
88        .chars()
89        .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
90    {
91        record_security_event(SecurityEvent::InvalidInput {
92            input_type: "interface_name".to_string(),
93            attempted_value: name.to_string(),
94            source: "validation".to_string(),
95        });
96        return Err(NetwatchError::Parse(
97            "Invalid characters in interface name".to_string(),
98        ));
99    }
100
101    // Additional security: block common attack patterns
102    let name_lower = name.to_lowercase();
103    if name_lower.contains("proc") || name_lower.contains("sys") || name_lower.contains("dev") {
104        record_security_event(SecurityEvent::InvalidInput {
105            input_type: "interface_name".to_string(),
106            attempted_value: name.to_string(),
107            source: "validation".to_string(),
108        });
109        return Err(NetwatchError::Parse(
110            "Suspicious interface name pattern".to_string(),
111        ));
112    }
113
114    Ok(())
115}
116
117/// Validates file paths for logging and configuration
118///
119/// # Security Considerations
120/// - Prevents path traversal attacks
121/// - Blocks access to sensitive system directories
122/// - Validates file extensions for expected types
123/// - Checks path length to prevent buffer overflow
124///
125/// # Examples
126/// ```
127/// use netwatch_rs::validation::validate_file_path;
128///
129/// assert!(validate_file_path("/tmp/netwatch.log", Some("log")).is_ok());
130/// assert!(validate_file_path("../../../etc/passwd", None).is_err());
131/// ```
132pub fn validate_file_path(path: &str, expected_extension: Option<&str>) -> Result<()> {
133    if path.is_empty() {
134        return Err(NetwatchError::Config(
135            "File path cannot be empty".to_string(),
136        ));
137    }
138
139    if path.len() > MAX_PATH_LEN {
140        return Err(NetwatchError::Config(format!(
141            "File path too long (max {MAX_PATH_LEN} characters)"
142        )));
143    }
144
145    // Check for null bytes and control characters
146    if path.contains('\0') || path.chars().any(|c| c.is_control()) {
147        return Err(NetwatchError::Config(
148            "Control characters not allowed in file path".to_string(),
149        ));
150    }
151
152    let path_obj = Path::new(path);
153
154    // Check for explicit path traversal patterns
155    if path.contains("..") {
156        record_security_event(SecurityEvent::InvalidInput {
157            input_type: "file_path".to_string(),
158            attempted_value: path.to_string(),
159            source: "validation".to_string(),
160        });
161        return Err(NetwatchError::Config("Path traversal detected".to_string()));
162    }
163
164    // Block access to sensitive system directories
165    let sensitive_dirs = [
166        "/etc",
167        "/boot",
168        "/proc",
169        "/sys",
170        "/dev",
171        "/root",
172        "/usr/bin",
173        "/usr/sbin",
174        "/bin",
175        "/sbin",
176    ];
177
178    for sensitive_dir in &sensitive_dirs {
179        if path.starts_with(sensitive_dir) {
180            return Err(NetwatchError::Config(
181                "Access to sensitive directory denied".to_string(),
182            ));
183        }
184    }
185
186    // Validate file extension if specified
187    if let Some(expected_ext) = expected_extension {
188        if let Some(extension) = path_obj.extension() {
189            if extension.to_string_lossy().to_lowercase() != expected_ext.to_lowercase() {
190                return Err(NetwatchError::Config(format!(
191                    "Invalid file extension, expected: {expected_ext}"
192                )));
193            }
194        } else {
195            return Err(NetwatchError::Config(format!(
196                "Missing file extension, expected: {expected_ext}"
197            )));
198        }
199    }
200
201    Ok(())
202}
203
204/// Validates refresh interval values
205///
206/// # Security Considerations
207/// - Prevents DoS attacks through excessive refresh rates
208/// - Ensures reasonable bounds for system resource usage
209/// - Prevents integer overflow in timing calculations
210pub fn validate_refresh_interval(interval_ms: u64) -> Result<()> {
211    if interval_ms < MIN_REFRESH_INTERVAL {
212        return Err(NetwatchError::Config(format!(
213            "Refresh interval too small (minimum {MIN_REFRESH_INTERVAL} ms)"
214        )));
215    }
216
217    if interval_ms > MAX_REFRESH_INTERVAL {
218        return Err(NetwatchError::Config(format!(
219            "Refresh interval too large (maximum {MAX_REFRESH_INTERVAL} ms)"
220        )));
221    }
222
223    Ok(())
224}
225
226/// Validates bandwidth values to prevent overflow and unrealistic values
227///
228/// # Security Considerations
229/// - Prevents integer overflow in calculations
230/// - Ensures reasonable bounds for bandwidth values
231/// - Blocks obviously malicious or unrealistic inputs
232pub fn validate_bandwidth(bandwidth_kbps: u64) -> Result<()> {
233    // Maximum reasonable bandwidth: 1 Tbps = 1,000,000,000 kbps
234    const MAX_BANDWIDTH: u64 = 1_000_000_000;
235
236    if bandwidth_kbps > MAX_BANDWIDTH {
237        return Err(NetwatchError::Config(format!(
238            "Bandwidth value too large (maximum {MAX_BANDWIDTH} kbps)"
239        )));
240    }
241
242    Ok(())
243}
244
245/// Validates configuration strings for injection attacks
246///
247/// # Security Considerations
248/// - Prevents command injection through config values
249/// - Blocks script injection attempts
250/// - Validates string content and encoding
251pub fn validate_config_string(value: &str, field_name: &str) -> Result<()> {
252    if value.len() > 1024 {
253        return Err(NetwatchError::Config(format!(
254            "Configuration value too long for field: {field_name}"
255        )));
256    }
257
258    // Check for null bytes and dangerous control characters
259    if value.contains('\0')
260        || value
261            .chars()
262            .any(|c| c.is_control() && c != '\n' && c != '\t')
263    {
264        return Err(NetwatchError::Config(format!(
265            "Invalid characters in configuration field: {field_name}"
266        )));
267    }
268
269    // Block common injection patterns
270    let dangerous_patterns = ["$(", "`", "${", "&&", "||", ";", "|", ">", "<", "&"];
271
272    for pattern in &dangerous_patterns {
273        if value.contains(pattern) {
274            return Err(NetwatchError::Config(format!(
275                "Suspicious pattern detected in field: {field_name}"
276            )));
277        }
278    }
279
280    Ok(())
281}
282
283/// Sanitizes user input by removing or escaping dangerous characters
284///
285/// # Security Considerations
286/// - Removes null bytes and control characters
287/// - Escapes shell metacharacters
288/// - Truncates overly long inputs
289pub fn sanitize_user_input(input: &str, max_length: usize) -> String {
290    input
291        .chars()
292        .filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
293        .take(max_length)
294        .collect::<String>()
295        .replace('$', "\\$")
296        .replace('`', "\\`")
297        .replace('"', "\\\"")
298        .replace('\'', "\\'")
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_interface_name_validation() {
307        // Valid interface names
308        assert!(validate_interface_name("eth0").is_ok());
309        assert!(validate_interface_name("wlan0").is_ok());
310        assert!(validate_interface_name("en0").is_ok());
311        assert!(validate_interface_name("lo").is_ok());
312        assert!(validate_interface_name("br-docker0").is_ok());
313
314        // Invalid interface names
315        assert!(validate_interface_name("").is_err());
316        assert!(validate_interface_name("../../../etc/passwd").is_err());
317        assert!(validate_interface_name(
318            "interface_with_very_long_name_that_exceeds_the_maximum_allowed_length"
319        )
320        .is_err());
321        assert!(validate_interface_name("interface with spaces").is_err());
322        assert!(validate_interface_name("interface\x00null").is_err());
323        assert!(validate_interface_name("interface\nwith\nnewlines").is_err());
324        assert!(validate_interface_name("/proc/net/dev").is_err());
325        assert!(validate_interface_name("proc").is_err());
326        assert!(validate_interface_name("sys").is_err());
327    }
328
329    #[test]
330    fn test_file_path_validation() {
331        // Valid file paths
332        assert!(validate_file_path("/tmp/netwatch.log", Some("log")).is_ok());
333        assert!(validate_file_path("/home/user/config.toml", Some("toml")).is_ok());
334        assert!(validate_file_path("./local.log", Some("log")).is_ok());
335
336        // Invalid file paths
337        assert!(validate_file_path("", None).is_err());
338        assert!(validate_file_path("../../../etc/passwd", None).is_err());
339        assert!(validate_file_path("/etc/shadow", None).is_err());
340        assert!(validate_file_path("/proc/version", None).is_err());
341        assert!(validate_file_path("file\x00with\x00nulls", None).is_err());
342        assert!(validate_file_path("/tmp/file.txt", Some("log")).is_err()); // Wrong extension
343    }
344
345    #[test]
346    fn test_refresh_interval_validation() {
347        // Valid intervals
348        assert!(validate_refresh_interval(500).is_ok());
349        assert!(validate_refresh_interval(1000).is_ok());
350        assert!(validate_refresh_interval(30000).is_ok());
351
352        // Invalid intervals
353        assert!(validate_refresh_interval(50).is_err()); // Too small
354        assert!(validate_refresh_interval(120000).is_err()); // Too large
355    }
356
357    #[test]
358    fn test_bandwidth_validation() {
359        // Valid bandwidth values
360        assert!(validate_bandwidth(1000).is_ok());
361        assert!(validate_bandwidth(1000000).is_ok());
362        assert!(validate_bandwidth(100000000).is_ok());
363
364        // Invalid bandwidth values
365        assert!(validate_bandwidth(u64::MAX).is_err()); // Too large
366        assert!(validate_bandwidth(2_000_000_000).is_err()); // Unrealistic
367    }
368
369    #[test]
370    fn test_config_string_validation() {
371        // Valid config strings
372        assert!(validate_config_string("normal_value", "test_field").is_ok());
373        assert!(validate_config_string("value-with-hyphens", "test_field").is_ok());
374        assert!(validate_config_string("value_with_underscores", "test_field").is_ok());
375
376        // Invalid config strings
377        assert!(validate_config_string("value$(echo hack)", "test_field").is_err());
378        assert!(validate_config_string("value`whoami`", "test_field").is_err());
379        assert!(validate_config_string("value && rm -rf /", "test_field").is_err());
380        assert!(validate_config_string("value\x00null", "test_field").is_err());
381        assert!(validate_config_string(&"x".repeat(2000), "test_field").is_err());
382        // Too long
383    }
384
385    #[test]
386    fn test_sanitize_user_input() {
387        assert_eq!(sanitize_user_input("normal input", 100), "normal input");
388        assert_eq!(sanitize_user_input("input$(echo)", 100), "input\\$(echo)");
389        assert_eq!(
390            sanitize_user_input("input`whoami`", 100),
391            "input\\`whoami\\`"
392        );
393        assert_eq!(
394            sanitize_user_input("input\"quoted\"", 100),
395            "input\\\"quoted\\\""
396        );
397        assert_eq!(
398            sanitize_user_input("very long input that exceeds limit", 10),
399            "very long "
400        );
401        assert_eq!(sanitize_user_input("input\x00null", 100), "inputnull");
402    }
403}