1515import os
1616import platform
1717import re
18- import shlex
1918import shutil
2019import subprocess
2120import sys
2726logger = get_logger (__name__ )
2827
2928
30- def contains_command_chaining (command : str ) -> bool :
31- r"""Check if command contains chaining operators that could be used to
32- bypass security.
29+ def check_command_safety (
30+ command : str ,
31+ allowed_commands : Optional [Set [str ]] = None ,
32+ ) -> Tuple [bool , str ]:
33+ r"""Check if a command (potentially with chaining) is safe to execute.
34+
35+ Args:
36+ command (str): The command string to check
37+ allowed_commands (Optional[Set[str]]): Set of allowed commands
38+ (whitelist mode)
39+
40+ Returns:
41+ Tuple[bool, str]: (is_safe, reason)
3342 """
34- # Pattern to match command chaining operators: ;, &&, ||, |
35- # But exclude cases where they are inside quotes or escaped
36- chaining_pattern = r'''
37- (?<!\\) # Not preceded by backslash (not escaped)
38- (?: # Group for alternation
39- ; # Semicolon
40- | # OR
41- \|\| # Logical OR
42- | # OR
43- && # Logical AND
44- | # OR
45- (?<!\|) # Not preceded by pipe (to avoid matching ||)
46- \| # Single pipe
47- (?!\|) # Not followed by pipe (to avoid matching ||)
48- )
49- (?= # Positive lookahead
50- (?: # Group
51- [^"'] # Not a quote
52- | # OR
53- "[^"]*" # Content in double quotes
54- | # OR
55- '[^']*' # Content in single quotes
56- )* # Zero or more times
57- $ # End of string
58- )
59- '''
43+ if not command .strip ():
44+ return False , "Empty command is not allowed."
6045
61- return bool (re .search (chaining_pattern , command , re .VERBOSE ))
46+ # Dangerous commands list - including ALL rm operations
47+ dangerous_commands = [
48+ # System administration
49+ 'sudo' ,
50+ 'su' ,
51+ 'reboot' ,
52+ 'shutdown' ,
53+ 'halt' ,
54+ 'poweroff' ,
55+ 'init' ,
56+ # File system manipulation
57+ 'rm' ,
58+ 'chown' ,
59+ 'chgrp' ,
60+ 'umount' ,
61+ 'mount' ,
62+ # Disk operations
63+ 'dd' ,
64+ 'mkfs' ,
65+ 'fdisk' ,
66+ 'parted' ,
67+ 'fsck' ,
68+ 'mkswap' ,
69+ 'swapon' ,
70+ 'swapoff' ,
71+ # Process management
72+ 'service' ,
73+ 'systemctl' ,
74+ 'systemd' ,
75+ # Network configuration
76+ 'iptables' ,
77+ 'ip6tables' ,
78+ 'ifconfig' ,
79+ 'route' ,
80+ 'iptables-save' ,
81+ # Cron and scheduling
82+ 'crontab' ,
83+ 'at' ,
84+ 'batch' ,
85+ # User management
86+ 'useradd' ,
87+ 'userdel' ,
88+ 'usermod' ,
89+ 'passwd' ,
90+ 'chpasswd' ,
91+ 'newgrp' ,
92+ # Kernel modules
93+ 'modprobe' ,
94+ 'rmmod' ,
95+ 'insmod' ,
96+ 'lsmod' ,
97+ ]
98+
99+ # Remove quoted strings to avoid false positives
100+ clean_command = re .sub (r'''["'][^"']*["']''' , ' ' , command )
101+
102+ # If whitelist mode, check ALL commands against the whitelist
103+ if allowed_commands is not None :
104+ # Extract all command words (at start or after operators)
105+ cmd_pattern = r'(?:^|;|\||&&)\s*\b([a-zA-Z_/][\w\-/]*)'
106+ found_commands = re .findall (cmd_pattern , clean_command , re .IGNORECASE )
107+ for cmd in found_commands :
108+ if cmd .lower () not in allowed_commands :
109+ return (
110+ False ,
111+ f"Command '{ cmd } ' is not in the allowed commands list." ,
112+ )
113+ return True , ""
114+
115+ # Check for dangerous commands
116+ for cmd in dangerous_commands :
117+ pattern = rf'(?:^|;|\||&&)\s*\b{ re .escape (cmd )} \b'
118+ if re .search (pattern , clean_command , re .IGNORECASE ):
119+ return False , f"Command '{ cmd } ' is blocked for safety."
120+
121+ return True , ""
62122
63123
64124def sanitize_command (
@@ -80,133 +140,25 @@ def sanitize_command(
80140 Returns:
81141 Tuple[bool, str]: (is_safe, message_or_command)
82142 """
83- # Apply security checks to both backends - security should be consistent
84143 if not safe_mode :
85144 return True , command # Skip all checks if safe_mode is disabled
86145
87- # First check for command chaining and pipes
88- if contains_command_chaining (command ):
89- return (
90- False ,
91- "Command chaining (;, &&, ||, |) is not allowed "
92- "for security reasons." ,
93- )
94-
95- parts = shlex .split (command )
96- if not parts :
97- return False , "Empty command is not allowed."
98- base_cmd = parts [0 ].lower ()
99-
100- # If whitelist is defined, only allow whitelisted commands
101- if allowed_commands is not None :
102- if base_cmd not in allowed_commands :
103- return (
104- False ,
105- f"Command '{ base_cmd } ' is not in the allowed commands list." ,
146+ # Use safety checker
147+ is_safe , reason = check_command_safety (command , allowed_commands )
148+ if not is_safe :
149+ return False , reason
150+
151+ # Additional check for Docker backend: prevent cd outside working directory
152+ if not use_docker_backend and working_dir and 'cd ' in command :
153+ # Extract cd commands and check their targets
154+ cd_pattern = r'\bcd\s+([^\s;|&]+)'
155+ for match in re .finditer (cd_pattern , command ):
156+ target_path = match .group (1 ).strip ('\' "' )
157+ target_dir = os .path .abspath (
158+ os .path .join (working_dir , target_path )
106159 )
107- # If command is whitelisted, skip the dangerous commands check
108- # but still apply other safety checks
109- else :
110- # Block dangerous commands (only when no whitelist is defined)
111- dangerous_commands = [
112- # System administration
113- 'sudo' ,
114- 'su' ,
115- 'reboot' ,
116- 'shutdown' ,
117- 'halt' ,
118- 'poweroff' ,
119- 'init' ,
120- # File system manipulation
121- 'rm' ,
122- 'mv' ,
123- 'chmod' ,
124- 'chown' ,
125- 'chgrp' ,
126- 'umount' ,
127- 'mount' ,
128- # Disk operations
129- 'dd' ,
130- 'mkfs' ,
131- 'fdisk' ,
132- 'parted' ,
133- 'fsck' ,
134- 'mkswap' ,
135- 'swapon' ,
136- 'swapoff' ,
137- # Process management
138- 'kill' ,
139- 'killall' ,
140- 'pkill' ,
141- 'service' ,
142- 'systemctl' ,
143- 'systemd' ,
144- # Network configuration
145- 'iptables' ,
146- 'ip6tables' ,
147- 'ifconfig' ,
148- 'route' ,
149- 'iptables-save' ,
150- # Cron and scheduling
151- 'crontab' ,
152- 'at' ,
153- 'batch' ,
154- # User management
155- 'useradd' ,
156- 'userdel' ,
157- 'usermod' ,
158- 'passwd' ,
159- 'chpasswd' ,
160- 'newgrp' ,
161- # Kernel modules
162- 'modprobe' ,
163- 'rmmod' ,
164- 'insmod' ,
165- 'lsmod' ,
166- # System information that could leak sensitive data
167- 'dmesg' ,
168- 'last' ,
169- 'lastlog' ,
170- 'who' ,
171- 'w' ,
172- ]
173- if base_cmd in dangerous_commands :
174- # Special handling for rm command - use regex for precise checking
175- if base_cmd == 'rm' :
176- # Check for dangerous rm options using regex
177- dangerous_rm_pattern = (
178- r'\s-[^-\s]*[rf][^-\s]*\s|\s--force\s|'
179- r'\s--recursive\s|\s-rf\s|\s-fr\s'
180- )
181- if re .search (dangerous_rm_pattern , command , re .IGNORECASE ):
182- return (
183- False ,
184- f"Command '{ base_cmd } ' with forceful or "
185- f"recursive options is blocked for safety." ,
186- )
187- # Also block rm without any target (could be dangerous)
188- if len (parts ) < 2 :
189- return (
190- False ,
191- "rm command requires target "
192- "file/directory specification." ,
193- )
194- else :
195- return False , f"Command '{ base_cmd } ' is blocked for safety."
196-
197- # For local backend only: prevent changing
198- # directory outside the workspace
199- # Docker containers are already sandboxed,
200- # so this check is not needed there
201- if (
202- not use_docker_backend
203- and base_cmd == 'cd'
204- and len (parts ) > 1
205- and working_dir
206- ):
207- target_dir = os .path .abspath (os .path .join (working_dir , parts [1 ]))
208- if not target_dir .startswith (working_dir ):
209- return False , "Cannot 'cd' outside of the working directory."
160+ if not target_dir .startswith (working_dir ):
161+ return False , "Cannot 'cd' outside of the working directory."
210162
211163 return True , command
212164
0 commit comments