11import logging
2+ import subprocess
3+ import shlex
24
35logger = logging .getLogger (__name__ )
46
57
6- import subprocess
8+ class SecurityError (Exception ):
9+ """Raised when a security violation is detected."""
10+ pass
711
812
913def run_command (command : str ):
1014 """
1115 Runs an external command and yields each line from stdout.
16+
17+ Security note: This function implements security checks to prevent
18+ command injection attacks.
1219
1320 Args:
1421 command: The command to run as a string.
1522
1623 Yields:
1724 Each line from the command's stdout.
25+
26+ Raises:
27+ SecurityError: If the command contains dangerous patterns.
28+ subprocess.CalledProcessError: If the command fails.
29+ """
30+ # Security validation
31+ _validate_command_security (command )
32+
33+ logger .debug (f"Executing validated command: { command } " )
34+
35+ # Use shell=False and split command properly to prevent injection
36+ try :
37+ # Split command safely using shlex
38+ command_parts = shlex .split (command )
39+
40+ # Additional validation on command parts
41+ if not command_parts :
42+ raise ValueError ("Empty command provided" )
43+
44+ # Check if the base command is in a safe list (optional additional security)
45+ base_command = command_parts [0 ]
46+ _validate_base_command (base_command )
47+
48+ process = subprocess .Popen (
49+ command_parts ,
50+ stdout = subprocess .PIPE ,
51+ stderr = subprocess .PIPE ,
52+ text = True ,
53+ shell = False # Critical: never use shell=True
54+ )
55+
56+ for line in process .stdout :
57+ logger .debug (f"Command output: { line .rstrip ()} " )
58+ yield line .rstrip () # Remove trailing newline
59+
60+ process .wait () # Wait for the command to complete
61+
62+ if process .returncode != 0 :
63+ # Get stderr for better error reporting
64+ stderr_output = process .stderr .read () if process .stderr else "No error details available"
65+ logger .error (f"Command failed with return code { process .returncode } : { stderr_output } " )
66+ raise subprocess .CalledProcessError (process .returncode , command )
67+
68+ logger .debug ("Command completed successfully" )
69+
70+ except subprocess .CalledProcessError :
71+ raise # Re-raise subprocess errors
72+ except Exception as e :
73+ logger .error (f"Error executing command '{ command } ': { e } " )
74+ raise SecurityError (f"Command execution failed: { e } " )
75+
76+
77+ def _validate_command_security (command : str ):
78+ """
79+ Validate that the command does not contain dangerous patterns.
80+
81+ Args:
82+ command: The command string to validate.
83+
84+ Raises:
85+ SecurityError: If dangerous patterns are detected.
86+ """
87+ # Check for dangerous shell metacharacters and patterns
88+ dangerous_patterns = [
89+ ';' , # Command separator
90+ '&&' , # Command chaining
91+ '||' , # Command chaining
92+ '|' , # Pipe (could be used maliciously)
93+ '$(' , # Command substitution
94+ '`' , # Command substitution (backticks)
95+ '>' , # Redirection
96+ '<' , # Redirection
97+ '&' , # Background execution
98+ '\n ' , # Newline injection
99+ '\r ' , # Carriage return injection
100+ ]
101+
102+ for pattern in dangerous_patterns :
103+ if pattern in command :
104+ raise SecurityError (f"Security violation: Command contains dangerous pattern '{ pattern } '" )
105+
106+ # Check for path traversal attempts
107+ if '..' in command or '~/' in command :
108+ raise SecurityError ("Security violation: Command contains path traversal patterns" )
109+
110+ # Check for attempts to access sensitive files
111+ sensitive_paths = ['/etc/passwd' , '/etc/shadow' , '/root/' , '~root' ]
112+ command_lower = command .lower ()
113+ for path in sensitive_paths :
114+ if path in command_lower :
115+ raise SecurityError (f"Security violation: Command attempts to access sensitive path '{ path } '" )
116+
117+
118+ def _validate_base_command (base_command : str ):
119+ """
120+ Validate that the base command is from an allowed list.
121+
122+ Args:
123+ base_command: The base command to validate.
124+
125+ Raises:
126+ SecurityError: If the command is not allowed.
18127 """
19- logger .debug (f"Executing command: { command } " )
20- process = subprocess .Popen (command , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True , shell = True )
21- for line in process .stdout :
22- logger .debug (f"Command output: { line .rstrip ()} " )
23- yield line .rstrip () # Remove trailing newline
24- process .wait () # Wait for the command to complete
25- if process .returncode != 0 :
26- logger .error (f"Command failed with return code { process .returncode } " )
27- raise subprocess .CalledProcessError (process .returncode , command )
28- logger .debug ("Command completed successfully" )
128+ # Define a whitelist of allowed commands (can be extended as needed)
129+ allowed_commands = {
130+ 'ls' , 'cat' , 'echo' , 'pwd' , 'head' , 'tail' , 'grep' , 'find' , 'wc' ,
131+ 'sort' , 'uniq' , 'cut' , 'awk' , 'sed' , 'tr' , 'date' , 'whoami' ,
132+ 'id' , 'uptime' , 'df' , 'du' , 'ps' , 'top' , 'free' , 'mount' ,
133+ 'python' , 'python3' , 'pip' , 'git' , 'curl' , 'wget' , 'ssh' ,
134+ 'rsync' , 'tar' , 'gzip' , 'gunzip' , 'zip' , 'unzip'
135+ }
136+
137+ # Extract just the command name (remove path if present)
138+ command_name = base_command .split ('/' )[- 1 ]
139+
140+ if command_name not in allowed_commands :
141+ # Log the attempt for security monitoring
142+ logger .warning (f"Attempted execution of non-whitelisted command: { base_command } " )
143+ raise SecurityError (f"Security violation: Command '{ command_name } ' is not in the allowed list" )
144+
145+ logger .debug (f"Base command '{ command_name } ' validated successfully" )
0 commit comments