Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion src/cortex-tui/src/mcp_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,37 @@ use anyhow::{Context, Result};
use cortex_common::AppDirs;
use serde::{Deserialize, Serialize};

// ============================================================
// SECURITY HELPERS
// ============================================================

/// Sanitize a server name to prevent path traversal attacks.
///
/// Only allows alphanumeric characters, hyphens, and underscores.
/// Any other characters (including path separators) are replaced with underscores.
fn sanitize_server_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect()
}

/// Validate a server name for safe filesystem use.
///
/// Returns true if the name contains only safe characters.
#[allow(dead_code)]
pub fn validate_server_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
Comment on lines +41 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_server_name is unused but should be called before saving to prevent name collision issues

The current implementation allows saving servers with invalid names (e.g., "../etc"), which get sanitized to "________etc" for the filename. This creates a collision risk where "../etc" and "________etc" would map to the same file. Consider validating names in save_server():

pub fn save_server(&self, server: &StoredMcpServer) -> Result<()> {
    if !validate_server_name(&server.name) {
        anyhow::bail!("Invalid server name: {}", server.name);
    }
    // ... rest of implementation
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cortex-tui/src/mcp_storage.rs
Line: 41:47

Comment:
`validate_server_name` is unused but should be called before saving to prevent name collision issues

The current implementation allows saving servers with invalid names (e.g., `"../etc"`), which get sanitized to `"________etc"` for the filename. This creates a collision risk where `"../etc"` and `"________etc"` would map to the same file. Consider validating names in `save_server()`:

```
pub fn save_server(&self, server: &StoredMcpServer) -> Result<()> {
    if !validate_server_name(&server.name) {
        anyhow::bail!("Invalid server name: {}", server.name);
    }
    // ... rest of implementation
}
```

How can I resolve this? If you propose a fix, please make it concise.


/// MCP transport type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
Expand Down Expand Up @@ -158,8 +189,11 @@ impl McpStorage {
}

/// Get the path to a server's config file
///
/// The server name is sanitized to prevent path traversal attacks.
fn server_path(&self, name: &str) -> PathBuf {
self.mcps_dir.join(format!("{}.json", name))
let sanitized_name = sanitize_server_name(name);
self.mcps_dir.join(format!("{}.json", sanitized_name))
}

/// Save an MCP server configuration
Expand Down Expand Up @@ -372,4 +406,44 @@ mod tests {
let result = storage.load_server("nonexistent").unwrap();
assert!(result.is_none());
}

#[test]
fn test_sanitize_server_name() {
// Normal names stay the same
assert_eq!(sanitize_server_name("my-server"), "my-server");
assert_eq!(sanitize_server_name("server_123"), "server_123");

// Path traversal attempts get sanitized
assert_eq!(sanitize_server_name("../../../etc"), "________etc");
assert_eq!(sanitize_server_name("test/subdir"), "test_subdir");
assert_eq!(sanitize_server_name("test\\windows"), "test_windows");
}

#[test]
fn test_validate_server_name() {
// Valid names
assert!(validate_server_name("my-server"));
assert!(validate_server_name("server_123"));
assert!(validate_server_name("ABC"));

// Invalid names
assert!(!validate_server_name("../../../etc"));
assert!(!validate_server_name("test/subdir"));
assert!(!validate_server_name(""));
assert!(!validate_server_name("name with spaces"));
}

#[test]
fn test_server_path_traversal() {
let (storage, tmp) = test_storage();
let base_dir = tmp.path().to_path_buf();

// Attempt path traversal
let malicious_name = "../../../etc/passwd";
let result_path = storage.server_path(malicious_name);

// The result should still be under mcps_dir
assert!(result_path.starts_with(base_dir.join("mcps")));
assert!(!result_path.to_string_lossy().contains(".."));
}
}
Loading