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}