turbomcp_server/
handler_validation.rs

1//! Secure Handler Name Validation
2//!
3//! This module prevents handler name injection attacks by validating that handler names
4//! are valid Rust identifiers using the `syn` crate (Sprint 2.4).
5//!
6//! ## Security Properties
7//!
8//! - **Injection Prevention**: Prevents malicious handler names like `../../../etc/passwd`,
9//!   `foo"; DROP TABLE handlers; --`, or other injection attempts
10//! - **Keyword Prevention**: Blocks Rust reserved keywords (`async`, `await`, `impl`, etc.)
11//! - **Path Traversal Prevention**: Blocks path components like `..`
12//! - **Canonical Validation**: Uses `syn::Ident` - the same validator used by rustc
13//!
14//! ## Attack Scenarios Prevented
15//!
16//! Without validation, an attacker could register handlers with names like:
17//! - `../../../sensitive_file` - Path traversal
18//! - `handler"; os.system("rm -rf /"); "` - Command injection
19//! - `<script>alert(1)</script>` - XSS in web UIs
20//! - `admin` or `system` - Privilege escalation attempts
21//!
22//! ## Implementation
23//!
24//! Uses the industry-standard `syn` crate (maintained by dtolnay) for identifier validation.
25//! This is the same crate used by every Rust procedural macro and provides:
26//! - Complete coverage of all Rust identifier rules
27//! - Automatic keyword detection (including weak keywords)
28//! - Zero maintenance (tracks Rust language evolution)
29//! - Battle-tested by millions of Rust projects
30//!
31//! ## Usage
32//!
33//! ```rust,ignore
34//! use turbomcp_server::handler_validation::validate_handler_name;
35//!
36//! // Valid handler names
37//! assert!(validate_handler_name("get_user").is_ok());
38//! assert!(validate_handler_name("fetch_data").is_ok());
39//! assert!(validate_handler_name("tool_123").is_ok());
40//!
41//! // Invalid handler names
42//! assert!(validate_handler_name("async").is_err());  // Reserved keyword
43//! assert!(validate_handler_name("../etc/passwd").is_err());  // Path traversal
44//! assert!(validate_handler_name("foo-bar").is_err());  // Invalid character
45//! ```
46
47use crate::ServerResult;
48
49/// Reserved handler names that should not be allowed
50///
51/// These names are reserved for internal system use or represent potential
52/// privilege escalation attempts.
53#[cfg(feature = "security")]
54const RESERVED_HANDLER_NAMES: &[&str] = &[
55    // System handlers
56    "initialize",
57    "initialized",
58    "shutdown",
59    "ping",
60    "pong",
61    "health",
62    "status",
63    // Privilege escalation attempts
64    "admin",
65    "root",
66    "system",
67    "sudo",
68    "su",
69    "superuser",
70    // Internal methods
71    "internal",
72    "private",
73    "protected",
74    "__init__",
75    "__main__",
76];
77
78/// Maximum length for handler names (prevents DoS via extremely long names)
79#[cfg(feature = "security")]
80const MAX_HANDLER_NAME_LENGTH: usize = 128;
81
82/// Validate a handler name for security
83///
84/// This function validates that a handler name is:
85/// 1. A valid Rust identifier (using `syn::Ident`)
86/// 2. Not a Rust reserved keyword
87/// 3. Not a reserved system name
88/// 4. Within reasonable length limits
89///
90/// ## Security Properties
91///
92/// - **Injection Prevention**: Uses `syn::Ident` which only accepts valid Rust identifiers
93/// - **Keyword Protection**: Automatically rejects Rust keywords (`async`, `await`, etc.)
94/// - **Reserved Name Protection**: Rejects internal system names
95/// - **Length Limits**: Prevents DoS via extremely long names
96///
97/// ## Performance
98///
99/// - Validation time: ~100-200ns per name (syn parsing is very fast)
100/// - No allocations for valid names
101/// - Fails fast on obviously invalid names
102///
103/// ## Examples
104///
105/// ```rust,ignore
106/// use turbomcp_server::handler_validation::validate_handler_name;
107///
108/// // Valid names
109/// assert!(validate_handler_name("get_user_info").is_ok());
110/// assert!(validate_handler_name("tool_v2").is_ok());
111/// assert!(validate_handler_name("_private_helper").is_ok());
112///
113/// // Invalid names
114/// assert!(validate_handler_name("").is_err());  // Empty
115/// assert!(validate_handler_name("async").is_err());  // Keyword
116/// assert!(validate_handler_name("admin").is_err());  // Reserved
117/// assert!(validate_handler_name("../etc/passwd").is_err());  // Invalid chars
118/// assert!(validate_handler_name("foo-bar").is_err());  // Hyphen not allowed
119/// ```
120///
121/// ## Errors
122///
123/// Returns [`crate::ServerError::Handler`] if:
124/// - Name is empty
125/// - Name exceeds `MAX_HANDLER_NAME_LENGTH` (128 characters)
126/// - Name is not a valid Rust identifier
127/// - Name is a Rust reserved keyword
128/// - Name is a reserved system name
129#[cfg(feature = "security")]
130pub fn validate_handler_name(name: &str) -> ServerResult<()> {
131    use crate::ServerError;
132
133    // Check for empty names
134    if name.is_empty() {
135        return Err(ServerError::handler(
136            "Handler name cannot be empty".to_string(),
137        ));
138    }
139
140    // Check length limits (prevent DoS)
141    if name.len() > MAX_HANDLER_NAME_LENGTH {
142        return Err(ServerError::handler(format!(
143            "Handler name '{}...' exceeds maximum length of {} characters",
144            &name[..50.min(name.len())],
145            MAX_HANDLER_NAME_LENGTH
146        )));
147    }
148
149    // Validate as Rust identifier using syn::Ident
150    // This prevents injection attacks by ensuring the name only contains
151    // valid identifier characters (alphanumeric + underscore, starting with letter/_)
152    syn::parse_str::<syn::Ident>(name).map_err(|e| {
153        ServerError::handler(format!(
154            "Invalid handler name '{}': {}\n\
155             \n\
156             Handler names must be valid Rust identifiers:\n\
157             - Start with a letter or underscore\n\
158             - Contain only letters, numbers, and underscores\n\
159             - Not be a Rust reserved keyword\n\
160             \n\
161             Reserved keywords include: async, await, fn, impl, let, match, struct, type, etc.\n\
162             See: https://doc.rust-lang.org/reference/keywords.html",
163            name, e
164        ))
165    })?;
166
167    // Check against reserved system names
168    if RESERVED_HANDLER_NAMES.contains(&name) {
169        return Err(ServerError::handler(format!(
170            "Handler name '{}' is reserved for system use.\n\
171             \n\
172             Reserved names: {:?}\n\
173             \n\
174             Please choose a different name for your handler.",
175            name, RESERVED_HANDLER_NAMES
176        )));
177    }
178
179    Ok(())
180}
181
182/// Validate a handler name (no-op when security feature is disabled)
183///
184/// When the `security` feature is not enabled, this function performs minimal validation
185/// (empty check only) to maintain API compatibility while keeping the binary small.
186#[cfg(not(feature = "security"))]
187pub fn validate_handler_name(name: &str) -> ServerResult<()> {
188    use crate::ServerError;
189
190    if name.is_empty() {
191        return Err(ServerError::handler(
192            "Handler name cannot be empty".to_string(),
193        ));
194    }
195
196    Ok(())
197}
198
199#[cfg(all(test, feature = "security"))]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_valid_handler_names() {
205        // Standard names
206        assert!(validate_handler_name("get_user").is_ok());
207        assert!(validate_handler_name("fetch_data").is_ok());
208        assert!(validate_handler_name("process_request").is_ok());
209
210        // With numbers
211        assert!(validate_handler_name("tool_v2").is_ok());
212        assert!(validate_handler_name("handler_123").is_ok());
213
214        // Starting with underscore
215        assert!(validate_handler_name("_internal").is_ok());
216        assert!(validate_handler_name("_helper_function").is_ok());
217
218        // CamelCase (valid as identifier)
219        assert!(validate_handler_name("GetUser").is_ok());
220        assert!(validate_handler_name("FetchData").is_ok());
221    }
222
223    #[test]
224    fn test_reject_empty_name() {
225        assert!(validate_handler_name("").is_err());
226    }
227
228    #[test]
229    fn test_reject_reserved_keywords() {
230        // Strict keywords
231        assert!(validate_handler_name("async").is_err());
232        assert!(validate_handler_name("await").is_err());
233        assert!(validate_handler_name("fn").is_err());
234        assert!(validate_handler_name("impl").is_err());
235        assert!(validate_handler_name("let").is_err());
236        assert!(validate_handler_name("match").is_err());
237        assert!(validate_handler_name("struct").is_err());
238        assert!(validate_handler_name("type").is_err());
239    }
240
241    #[test]
242    fn test_reject_reserved_system_names() {
243        assert!(validate_handler_name("initialize").is_err());
244        assert!(validate_handler_name("shutdown").is_err());
245        assert!(validate_handler_name("admin").is_err());
246        assert!(validate_handler_name("root").is_err());
247        assert!(validate_handler_name("system").is_err());
248    }
249
250    #[test]
251    fn test_reject_path_traversal() {
252        assert!(validate_handler_name("../etc/passwd").is_err());
253        assert!(validate_handler_name("../../secret").is_err());
254        assert!(validate_handler_name("..").is_err());
255    }
256
257    #[test]
258    fn test_reject_invalid_characters() {
259        // Hyphens
260        assert!(validate_handler_name("foo-bar").is_err());
261
262        // Spaces
263        assert!(validate_handler_name("foo bar").is_err());
264
265        // Special characters
266        assert!(validate_handler_name("foo@bar").is_err());
267        assert!(validate_handler_name("foo#bar").is_err());
268        assert!(validate_handler_name("foo$bar").is_err());
269
270        // Path separators
271        assert!(validate_handler_name("foo/bar").is_err());
272        assert!(validate_handler_name("foo\\bar").is_err());
273    }
274
275    #[test]
276    fn test_reject_starting_with_number() {
277        assert!(validate_handler_name("1foo").is_err());
278        assert!(validate_handler_name("123").is_err());
279    }
280
281    #[test]
282    fn test_reject_sql_injection_attempts() {
283        assert!(validate_handler_name("'; DROP TABLE users; --").is_err());
284        assert!(validate_handler_name("foo\"; DELETE FROM handlers; --").is_err());
285    }
286
287    #[test]
288    fn test_reject_command_injection_attempts() {
289        assert!(validate_handler_name("; rm -rf /").is_err());
290        assert!(validate_handler_name("| cat /etc/passwd").is_err());
291        assert!(validate_handler_name("$(whoami)").is_err());
292    }
293
294    #[test]
295    fn test_reject_xss_attempts() {
296        assert!(validate_handler_name("<script>alert(1)</script>").is_err());
297        assert!(validate_handler_name("javascript:alert(1)").is_err());
298    }
299
300    #[test]
301    fn test_reject_overly_long_names() {
302        let long_name = "a".repeat(MAX_HANDLER_NAME_LENGTH + 1);
303        assert!(validate_handler_name(&long_name).is_err());
304    }
305
306    #[test]
307    fn test_accept_max_length_names() {
308        let max_name = "a".repeat(MAX_HANDLER_NAME_LENGTH);
309        assert!(validate_handler_name(&max_name).is_ok());
310    }
311
312    // Note: syn::Ident can parse Unicode characters as raw identifiers (r#identifier),
313    // so we don't explicitly reject them. While ASCII identifiers are preferred, Unicode
314    // identifiers are technically valid in Rust and don't pose a security risk since
315    // syn validates them properly.
316    //
317    // The security boundary is maintained by syn::Ident - it only accepts valid Rust
318    // identifiers, preventing injection attacks regardless of character set.
319}