turbomcp_protocol/security/
validation.rs

1//! Path validation for security
2//!
3//! This module provides focused path validation utilities to prevent common
4//! security vulnerabilities like path traversal attacks. It follows the principle
5//! of doing one thing well rather than trying to cover every possible security scenario.
6
7use crate::Result;
8use std::path::{Path, PathBuf};
9use tracing::{debug, warn};
10
11/// Validates a path for basic security constraints
12///
13/// This function performs essential security checks:
14/// - Canonicalizes the path to resolve symlinks and relative components
15/// - Prevents path traversal attacks by checking for ".." patterns
16/// - Validates that the path is within reasonable bounds
17///
18/// # Examples
19///
20/// ```rust,no_run
21/// use turbomcp_protocol::security::validate_path;
22///
23/// // Safe path
24/// let safe_path = validate_path("/home/user/data.txt")?;
25///
26/// // Path traversal attempt - will fail
27/// let result = validate_path("/home/user/../../../etc/passwd");
28/// assert!(result.is_err());
29/// # Ok::<(), Box<dyn std::error::Error>>(())
30/// ```
31pub fn validate_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
32    let path = path.as_ref();
33    debug!("Validating path: {:?}", path);
34
35    // Check for obvious path traversal patterns before filesystem operations
36    let path_str = path.to_string_lossy();
37    if path_str.contains("..") {
38        return Err(crate::Error::security(format!(
39            "Path traversal pattern detected: {:?}",
40            path
41        )));
42    }
43
44    // Canonicalize the path to resolve symlinks and relative components
45    let canonical_path = match path.canonicalize() {
46        Ok(p) => p,
47        Err(e) => {
48            warn!("Failed to canonicalize path {:?}: {}", path, e);
49            return Err(crate::Error::security(format!(
50                "Invalid path or access denied: {:?}",
51                path
52            )));
53        }
54    };
55
56    // Basic sanity check on path depth to prevent excessive nesting
57    let depth = canonical_path.components().count();
58    if depth > 20 {
59        // Reasonable limit for most use cases
60        return Err(crate::Error::security(format!(
61            "Path depth too deep ({}): {:?}",
62            depth, canonical_path
63        )));
64    }
65
66    debug!("Path validation successful: {:?}", canonical_path);
67    Ok(canonical_path)
68}
69
70/// Validates a path and enforces it's within a base directory
71///
72/// This is useful for ensuring file operations stay within allowed boundaries.
73///
74/// # Examples
75///
76/// ```rust,no_run
77/// use turbomcp_protocol::security::validate_path_within;
78///
79/// let base = "/home/user/workspace";
80/// let file_path = validate_path_within("/home/user/workspace/project/file.txt", base)?;
81/// # Ok::<(), Box<dyn std::error::Error>>(())
82/// ```
83pub fn validate_path_within<P: AsRef<Path>, B: AsRef<Path>>(path: P, base: B) -> Result<PathBuf> {
84    let validated_path = validate_path(path)?;
85    let base_path = base
86        .as_ref()
87        .canonicalize()
88        .map_err(|e| crate::Error::security(format!("Invalid base path: {}", e)))?;
89
90    if !validated_path.starts_with(&base_path) {
91        return Err(crate::Error::security(format!(
92            "Path outside allowed directory: {:?} not within {:?}",
93            validated_path, base_path
94        )));
95    }
96
97    Ok(validated_path)
98}
99
100/// Checks if a file extension is allowed
101///
102/// Simple utility for validating file extensions against an allow list.
103pub fn validate_file_extension<P: AsRef<Path>>(path: P, allowed_extensions: &[&str]) -> Result<()> {
104    let path = path.as_ref();
105
106    match path.extension().and_then(|ext| ext.to_str()) {
107        Some(ext) => {
108            if allowed_extensions.contains(&ext) {
109                Ok(())
110            } else {
111                Err(crate::Error::security(format!(
112                    "File extension '{}' not allowed",
113                    ext
114                )))
115            }
116        }
117        None => {
118            if allowed_extensions.is_empty() {
119                Ok(()) // No extension required
120            } else {
121                Err(crate::Error::security(
122                    "File must have an extension".to_string(),
123                ))
124            }
125        }
126    }
127}