Skip to main content

ssh_mcp/ssh/
elevation.rs

1//! Privilege elevation utilities for SSH command execution
2//!
3//! This module provides utilities for:
4//! - Wrapping commands with `sudo` for privilege escalation
5//! - Escaping passwords and commands for safe shell execution
6//!
7//! The elevation logic for `su` shells is implemented directly in
8//! [`SshConnectionManager`](super::connection::SshConnectionManager).
9
10/// Wraps a command for execution with sudo privileges.
11///
12/// # Arguments
13/// * `command` - The command to wrap with sudo
14/// * `password` - Optional sudo password. If None, uses `sudo -n` (passwordless).
15///   If Some, uses `printf | sudo -S` to pipe the password.
16///
17/// # Returns
18/// A string containing the wrapped command ready for execution.
19///
20/// # Examples
21///
22/// ```
23/// use ssh_mcp::ssh::elevation::wrap_sudo_command;
24///
25/// // Passwordless sudo
26/// let cmd = wrap_sudo_command("apt update", None);
27/// assert_eq!(cmd, "sudo -n sh -c 'apt update'");
28///
29/// // Sudo with password
30/// let cmd = wrap_sudo_command("apt update", Some("mypassword"));
31/// assert_eq!(cmd, "printf '%s\\n' 'mypassword' | sudo -S -p '' sh -c 'apt update'");
32/// ```
33pub fn wrap_sudo_command(command: &str, password: Option<&str>) -> String {
34    let escaped_command = escape_for_shell(command);
35
36    match password {
37        None => {
38            // No password provided, use -n to fail if sudo requires a password
39            format!("sudo -n sh -c '{}'", escaped_command)
40        }
41        Some(pwd) => {
42            // Password provided — pipe it into sudo using printf. This avoids complex
43            // PTY/stdin handling on the SSH channel and is simpler and more reliable.
44            let escaped_pwd = escape_for_shell(pwd);
45            format!(
46                "printf '%s\\n' '{}' | sudo -S -p '' sh -c '{}'",
47                escaped_pwd, escaped_command
48            )
49        }
50    }
51}
52
53/// Escapes a string for safe use in single-quoted shell contexts.
54///
55/// Replaces single quotes with the pattern `'"'"'` which:
56/// 1. Ends the current single-quoted string
57/// 2. Adds an escaped single quote
58/// 3. Starts a new single-quoted string
59///
60/// This is the standard POSIX-compliant method for escaping single quotes
61/// within single-quoted strings.
62///
63/// # Examples
64///
65/// ```
66/// use ssh_mcp::ssh::elevation::escape_for_shell;
67///
68/// assert_eq!(escape_for_shell("hello"), "hello");
69/// assert_eq!(escape_for_shell("it's"), "it'\"'\"'s");
70/// assert_eq!(escape_for_shell("a'b'c"), "a'\"'\"'b'\"'\"'c");
71/// ```
72pub fn escape_for_shell(s: &str) -> String {
73    crate::shell_escape::escape_for_shell(s)
74}
75
76/// Checks if a password is valid for use in sudo commands.
77///
78/// A valid password:
79/// - Is not empty after trimming
80/// - Does not contain null bytes
81///
82/// # Arguments
83/// * `password` - The password to validate
84///
85/// # Returns
86/// `true` if the password is valid, `false` otherwise.
87pub fn is_valid_password(password: &str) -> bool {
88    !password.trim().is_empty() && !password.contains('\0')
89}
90
91/// Sanitizes a password by trimming whitespace.
92///
93/// Returns `None` if the password is empty or only whitespace.
94///
95/// # Arguments
96/// * `password` - The password to sanitize
97///
98/// # Returns
99/// `Some(String)` with the sanitized password, or `None` if invalid.
100pub fn sanitize_password(password: Option<&str>) -> Option<String> {
101    password
102        .map(|p| p.trim())
103        .filter(|p| !p.is_empty())
104        .map(|p| p.to_string())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_wrap_sudo_command_without_password() {
113        let result = wrap_sudo_command("apt update", None);
114        assert_eq!(result, "sudo -n sh -c 'apt update'");
115    }
116
117    #[test]
118    fn test_wrap_sudo_command_with_password() {
119        let result = wrap_sudo_command("apt update", Some("secret123"));
120        assert_eq!(
121            result,
122            "printf '%s\\n' 'secret123' | sudo -S -p '' sh -c 'apt update'"
123        );
124    }
125
126    #[test]
127    fn test_wrap_sudo_command_with_quotes_in_command() {
128        let result = wrap_sudo_command("echo 'hello world'", None);
129        assert_eq!(result, "sudo -n sh -c 'echo '\"'\"'hello world'\"'\"''");
130    }
131
132    #[test]
133    fn test_wrap_sudo_command_with_quotes_in_password() {
134        let result = wrap_sudo_command("apt update", Some("pass'word"));
135        assert_eq!(
136            result,
137            "printf '%s\\n' 'pass'\"'\"'word' | sudo -S -p '' sh -c 'apt update'"
138        );
139    }
140
141    #[test]
142    fn test_wrap_sudo_command_complex() {
143        let result = wrap_sudo_command("cat /etc/shadow | grep root", Some("admin123"));
144        assert_eq!(
145            result,
146            "printf '%s\\n' 'admin123' | sudo -S -p '' sh -c 'cat /etc/shadow | grep root'"
147        );
148    }
149
150    #[test]
151    fn test_escape_for_shell_no_quotes() {
152        assert_eq!(escape_for_shell("hello world"), "hello world");
153    }
154
155    #[test]
156    fn test_escape_for_shell_single_quote() {
157        assert_eq!(escape_for_shell("it's"), "it'\"'\"'s");
158    }
159
160    #[test]
161    fn test_escape_for_shell_multiple_quotes() {
162        assert_eq!(
163            escape_for_shell("'a' and 'b'"),
164            "'\"'\"'a'\"'\"' and '\"'\"'b'\"'\"'"
165        );
166    }
167
168    #[test]
169    fn test_is_valid_password() {
170        assert!(is_valid_password("secret123"));
171        assert!(is_valid_password("with spaces"));
172        assert!(!is_valid_password(""));
173        assert!(!is_valid_password("   "));
174        assert!(!is_valid_password("has\0null"));
175    }
176
177    #[test]
178    fn test_sanitize_password() {
179        assert_eq!(
180            sanitize_password(Some("secret")),
181            Some("secret".to_string())
182        );
183        assert_eq!(
184            sanitize_password(Some("  secret  ")),
185            Some("secret".to_string())
186        );
187        assert_eq!(sanitize_password(Some("")), None);
188        assert_eq!(sanitize_password(Some("   ")), None);
189        assert_eq!(sanitize_password(None), None);
190    }
191}