Skip to content
Merged
Show file tree
Hide file tree
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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ Execute a command on a remote host.
- `timeout` (optional): Command timeout in seconds
- `working_dir` (optional): Working directory for command execution

**Command Validation:**
The server includes built-in protection against dangerous commands (e.g., `rm -rf /`, `mkfs`, `dd` operations on devices). Dangerous commands will be blocked by default. To disable validation, set the `REMOTESHELL_DISABLE_VALIDATION` environment variable:

```bash
export REMOTESHELL_DISABLE_VALIDATION=1
```

**Example Usage:**
```
Execute "ls -la /home" on prod-server
Expand Down Expand Up @@ -299,11 +306,24 @@ Upload all .conf files from /etc/local to /etc/remote on backup-server
- Keys can be password-protected for additional security
- Use different keys for different hosts when possible

3. **Connection Timeouts**:
3. **Command Validation**:
- The server includes built-in protection against dangerous commands
- Commands that could damage the system are blocked by default, including:
- Deleting root or critical system directories (`rm -rf /`, `rm -rf /etc`, etc.)
- Formatting filesystems (`mkfs`, `fdisk`, `parted`)
- Destructive `dd` operations
- System shutdown/reboot commands (`halt`, `poweroff`, `reboot`, `shutdown`)
- To disable command validation (not recommended), set the `REMOTESHELL_DISABLE_VALIDATION` environment variable:
```bash
export REMOTESHELL_DISABLE_VALIDATION=1
```
- **Warning**: Disabling validation removes an important safety layer. Only do this if you fully trust the commands being executed.

4. **Connection Timeouts**:
- Set appropriate timeouts to prevent hanging connections
- Connections automatically reconnect if they drop

4. **Global Config File**:
5. **Global Config File**:
- Located at `~/.remoteShell/config.json`
- Should have restrictive permissions (600)
- Consider encrypting sensitive data at rest
Expand Down
114 changes: 114 additions & 0 deletions src/remoteshell_mcp/command_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Command validator to prevent execution of dangerous commands."""

import os
import re
from typing import List, Tuple


class DangerousCommandError(Exception):
"""Raised when a dangerous command is detected."""
pass


class CommandValidator:
"""Validates commands to prevent execution of dangerous operations."""

# List of dangerous command patterns: each tuple contains (pattern, description)
DANGEROUS_PATTERNS: List[Tuple[str, str]] = [
# Remove root directory (exact match)
(r'\brm\s+.*-.*rf\s+/\s*$', 'Remove root directory'),
(r'\brm\s+.*-.*rf\s+/\s+', 'Remove root directory'),
(r'\brm\s+.*-.*rf\s+/\*', 'Remove all files under root directory'),
(r'\brm\s+.*-.*rf\s+/\s*\*', 'Remove all files under root directory'),

# Remove critical system directories (exact match, only matches directory itself, not files under it)
(r'\brm\s+.*-.*rf\s+/root(?:\s|$)', 'Remove /root directory'),
(r'\brm\s+.*-.*rf\s+/etc(?:\s|$)', 'Remove /etc directory'),
(r'\brm\s+.*-.*rf\s+/usr(?:\s|$)', 'Remove /usr directory'),
(r'\brm\s+.*-.*rf\s+/bin(?:\s|$)', 'Remove /bin directory'),
(r'\brm\s+.*-.*rf\s+/sbin(?:\s|$)', 'Remove /sbin directory'),
(r'\brm\s+.*-.*rf\s+/lib(?:\s|$)', 'Remove /lib directory'),
(r'\brm\s+.*-.*rf\s+/var(?:\s|$)', 'Remove /var directory'),
(r'\brm\s+.*-.*rf\s+/sys(?:\s|$)', 'Remove /sys directory'),
(r'\brm\s+.*-.*rf\s+/proc(?:\s|$)', 'Remove /proc directory'),
(r'\brm\s+.*-.*rf\s+/dev(?:\s|$)', 'Remove /dev directory'),
(r'\brm\s+.*-.*rf\s+/boot(?:\s|$)', 'Remove /boot directory'),

# Format commands
(r'\bmkfs\b', 'Format filesystem'),
(r'\bfdisk\b', 'Disk partitioning operation'),
(r'\bparted\b', 'Disk partitioning operation'),

# Destructive dd commands
(r'\bdd\s+.*if=.*of=/dev/', 'Destructive dd command'),
(r'\bdd\s+.*if=/dev/zero', 'dd command using /dev/zero'),
(r'\bdd\s+.*if=/dev/urandom', 'dd command using /dev/urandom'),

# Critical system operations
(r'\bchmod\s+.*777\s+.*/', 'Modify root directory permissions'),
(r'\bchown\s+.*root\s+.*/', 'Modify root directory ownership'),

# Other dangerous operations
(r'>\s*/dev/', 'Redirect to device file'),
(r':\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;', 'Fork bomb'),
(r'\bhalt\b', 'System halt'),
(r'\bpoweroff\b', 'System poweroff'),
(r'\breboot\b', 'System reboot'),
(r'\bshutdown\b', 'System shutdown'),

# Patterns for removing all files (but allow relative paths like ./test)
(r'\brm\s+.*-.*rf\s+\*', 'Remove all files in current directory'),
(r'\brm\s+.*-.*rf\s+\.\.(?:\s|$|/)', 'Remove parent directory'),
]

@classmethod
def validate(cls, command: str) -> None:
"""
Validate if a command is safe to execute.

Args:
command: Command string to execute

Raises:
DangerousCommandError: If a dangerous command is detected

Note:
Validation can be bypassed by setting the REMOTESHELL_DISABLE_VALIDATION
environment variable to any non-empty value.
"""
if not command or not command.strip():
return

# Skip validation if REMOTESHELL_DISABLE_VALIDATION environment variable is set
if os.environ.get('REMOTESHELL_DISABLE_VALIDATION'):
return

# Normalize command: remove extra spaces, convert to lowercase for matching
normalized_command = ' '.join(command.split())
command_lower = normalized_command.lower()

# Check if matches dangerous patterns
for pattern, description in cls.DANGEROUS_PATTERNS:
if re.search(pattern, command_lower, re.IGNORECASE):
raise DangerousCommandError(
f"Dangerous command detected: {description}\n"
f"Command: {command}\n"
f"Pattern: {pattern}"
)

# Additional check: prevent bypassing through variables or quotes
# Check if command contains obvious dangerous operations (use word boundaries for exact matching)
dangerous_keywords_patterns = [
(r'\brm\s+-rf\s+/\s*$', 'rm -rf /'),
(r'\brm\s+-rf\s+/\s+', 'rm -rf /'),
(r'\brm\s+-rf\s+/\*', 'rm -rf /*'),
(r'\brm\s+-rf\s+/root\b', 'rm -rf /root'),
(r'\bdd\s+.*if=/dev/zero\b', 'dd if=/dev/zero'),
]

for pattern, keyword_desc in dangerous_keywords_patterns:
if re.search(pattern, command_lower, re.IGNORECASE):
raise DangerousCommandError(
f"Dangerous command keyword detected: {keyword_desc}\n"
f"Command: {command}"
)
12 changes: 12 additions & 0 deletions src/remoteshell_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .config_loader import ConfigLoader, ConnectionConfig
from .connection_manager import ConnectionManager
from .ssh_client import SSHConnectionError, SSHCommandError, SSHFileTransferError
from .command_validator import CommandValidator, DangerousCommandError


# Initialize FastMCP server
Expand Down Expand Up @@ -107,6 +108,9 @@ def execute_command(
manager = get_connection_manager()

try:
# Validate command before execution
CommandValidator.validate(command)

# Get or create connection
client = manager.get_or_create_connection(connection_id)

Expand All @@ -126,6 +130,14 @@ def execute_command(
"exit_code": result["exit_code"]
}

except DangerousCommandError as e:
return {
"success": False,
"error": str(e),
"connection_id": connection_id,
"command": command,
"message": f"Command validation failed: {e}"
}
except (ValueError, SSHConnectionError, SSHCommandError) as e:
return {
"success": False,
Expand Down