Skip to main content

mermaid_cli/utils/
validation.rs

1/// Security validation utilities for file paths and commands
2///
3/// This module provides centralized validation functions to prevent common
4/// security issues like directory traversal and command injection attacks.
5use crate::constants::DANGEROUS_COMMANDS;
6use std::path::Path;
7
8/// Validate file path to prevent directory traversal attacks
9///
10/// # Checks
11/// - Rejects paths with `..` (directory traversal attempts)
12/// - Rejects absolute paths outside the project
13/// - Rejects paths with null bytes
14/// - Rejects paths targeting sensitive files (.ssh, .aws, .env, etc.)
15///
16/// # Arguments
17/// * `path` - The file path to validate
18///
19/// # Returns
20/// * `Ok(path)` - The validated path if all checks pass
21/// * `Err(reason)` - A string describing why the path was rejected
22pub fn validate_file_path(path: &str) -> Result<String, String> {
23    // Reject paths with directory traversal attempts
24    if path.contains("..") {
25        return Err(format!("Path contains directory traversal: {}", path));
26    }
27
28    // Reject absolute paths outside project
29    let path_obj = Path::new(path);
30    if path_obj.is_absolute() {
31        return Err(format!("Absolute paths not allowed: {}", path));
32    }
33
34    // Reject paths with null bytes
35    if path.contains('\0') {
36        return Err("Path contains null byte".to_string());
37    }
38
39    // Reject paths targeting sensitive files
40    let sensitive_patterns = [
41        ".ssh/",
42        "id_rsa",
43        "id_dsa",
44        "id_ecdsa",
45        "id_ed25519",
46        ".aws/",
47        "credentials",
48        ".env",
49        "config.toml",
50        "/etc/passwd",
51        "/etc/shadow",
52    ];
53
54    for pattern in &sensitive_patterns {
55        if path.contains(pattern) {
56            return Err(format!("Access to sensitive file denied: {}", path));
57        }
58    }
59
60    Ok(path.to_string())
61}
62
63/// Validate command to prevent dangerous operations and command injection
64///
65/// # Checks
66/// - Rejects known dangerous commands from DANGEROUS_COMMANDS list
67/// - Rejects piping to bash/sh shells
68/// - Rejects command substitution attempts (`$(...)` or backticks)
69/// - Rejects command chaining (`&&`)
70/// - Rejects `eval` command
71///
72/// # Arguments
73/// * `command` - The command string to validate
74///
75/// # Returns
76/// * `Ok(command)` - The validated command if all checks pass
77/// * `Err(reason)` - A string describing why the command was rejected
78pub fn validate_command(command: &str) -> Result<String, String> {
79    // Check against known dangerous commands
80    for dangerous in DANGEROUS_COMMANDS {
81        if command.contains(dangerous) {
82            return Err(format!("Dangerous command blocked: {}", dangerous));
83        }
84    }
85
86    // Additional checks for pipe to bash/sh
87    if (command.contains('|') && (command.contains("bash") || command.contains("sh")))
88        || command.contains("eval")
89    {
90        return Err("Piping to shell interpreter is not allowed".to_string());
91    }
92
93    // Check for command injection attempts (more precise matching)
94    if command.contains("$(") || command.contains("`") {
95        return Err("Command substitution detected".to_string());
96    }
97
98    // Check for command chaining (but allow it in some contexts)
99    // Block && only if it's standalone (not part of a word)
100    if command.contains(" && ") || command.ends_with("&&") || command.starts_with("&&") {
101        return Err("Command chaining detected".to_string());
102    }
103
104    Ok(command.to_string())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_validate_file_path_directory_traversal() {
113        assert!(validate_file_path("../secret.txt").is_err());
114        assert!(validate_file_path("../../etc/passwd").is_err());
115    }
116
117    #[test]
118    fn test_validate_file_path_absolute_paths() {
119        assert!(validate_file_path("/etc/passwd").is_err());
120        assert!(validate_file_path("/home/user/.ssh/id_rsa").is_err());
121    }
122
123    #[test]
124    fn test_validate_file_path_sensitive_files() {
125        assert!(validate_file_path(".ssh/id_rsa").is_err());
126        assert!(validate_file_path(".aws/credentials").is_err());
127        assert!(validate_file_path(".env").is_err());
128    }
129
130    #[test]
131    fn test_validate_file_path_valid() {
132        assert!(validate_file_path("src/main.rs").is_ok());
133        assert!(validate_file_path("Cargo.toml").is_ok());
134        assert!(validate_file_path("tests/test.rs").is_ok());
135    }
136
137    #[test]
138    fn test_validate_command_dangerous() {
139        assert!(validate_command("rm -rf /").is_err());
140        assert!(validate_command("mkfs").is_err());
141    }
142
143    #[test]
144    fn test_validate_command_injection() {
145        assert!(validate_command("cargo test && rm -rf /").is_err());
146        assert!(validate_command("echo test | bash").is_err());
147        assert!(validate_command("echo $(cat /etc/passwd)").is_err());
148    }
149
150    #[test]
151    fn test_validate_command_valid() {
152        assert!(validate_command("cargo test").is_ok());
153        assert!(validate_command("cargo build --release").is_ok());
154        assert!(validate_command("rustfmt src/main.rs").is_ok());
155    }
156}