@@ -12,11 +12,26 @@ class ShellExecutor:
12
12
13
13
def __init__ (self ):
14
14
"""
15
- Initialize the executor. The allowed commands are read from ALLOW_COMMANDS
16
- environment variable during command validation, not at initialization.
15
+ Initialize the executor.
17
16
"""
18
17
pass
19
18
19
+ def _get_allowed_commands (self ) -> set [str ]:
20
+ """Get the set of allowed commands from environment variables"""
21
+ allow_commands = os .environ .get ("ALLOW_COMMANDS" , "" )
22
+ allowed_commands = os .environ .get ("ALLOWED_COMMANDS" , "" )
23
+ commands = allow_commands + "," + allowed_commands
24
+ return {cmd .strip () for cmd in commands .split ("," ) if cmd .strip ()}
25
+
26
+ def get_allowed_commands (self ) -> list [str ]:
27
+ """Get the list of allowed commands from environment variables"""
28
+ return list (self ._get_allowed_commands ())
29
+
30
+ def is_command_allowed (self , command : str ) -> bool :
31
+ """Check if a command is in the allowed list"""
32
+ cmd = command .strip ()
33
+ return cmd in self ._get_allowed_commands ()
34
+
20
35
def _validate_redirection_syntax (self , command : List [str ]) -> None :
21
36
"""
22
37
Validate the syntax of redirection operators in the command.
@@ -155,24 +170,12 @@ async def _cleanup_handles(
155
170
"""
156
171
for key in ["stdout" , "stderr" ]:
157
172
handle = handles .get (key )
158
- if isinstance (handle , IO ) and handle != asyncio . subprocess . PIPE :
173
+ if handle and hasattr (handle , "close" ) and not isinstance ( handle , int ) :
159
174
try :
160
175
handle .close ()
161
- except IOError :
176
+ except ( IOError , ValueError ) :
162
177
pass
163
178
164
- def _get_allowed_commands (self ) -> set :
165
- """
166
- Get the set of allowed commands from environment variables.
167
- Checks both ALLOW_COMMANDS and ALLOWED_COMMANDS.
168
- """
169
- allow_commands = os .environ .get ("ALLOW_COMMANDS" , "" )
170
- allowed_commands = os .environ .get ("ALLOWED_COMMANDS" , "" )
171
-
172
- # Combine and deduplicate commands from both environment variables
173
- commands = allow_commands + "," + allowed_commands
174
- return {cmd .strip () for cmd in commands .split ("," ) if cmd .strip ()}
175
-
176
179
def _clean_command (self , command : List [str ]) -> List [str ]:
177
180
"""
178
181
Clean command by trimming whitespace from each part.
@@ -253,32 +256,36 @@ def _validate_directory(self, directory: Optional[str]) -> None:
253
256
if not os .access (directory , os .R_OK | os .X_OK ):
254
257
raise ValueError (f"Directory is not accessible: { directory } " )
255
258
256
- def get_allowed_commands (self ) -> list [str ]:
257
- """Get the allowed commands"""
258
- return list (self ._get_allowed_commands ())
259
-
260
259
def _validate_no_shell_operators (self , cmd : str ) -> None :
261
260
"""Validate that the command does not contain shell operators"""
262
261
if cmd in [";" "&&" , "||" , "|" ]:
263
262
raise ValueError (f"Unexpected shell operator: { cmd } " )
264
263
265
- def _validate_pipeline (self , commands : List [str ]) -> None :
266
- """Validate pipeline command and ensure all parts are allowed"""
264
+ def _validate_pipeline (self , commands : List [str ]) -> Dict [str , str ]:
265
+ """Validate pipeline command and ensure all parts are allowed
266
+
267
+ Returns:
268
+ Dict[str, str]: Error message if validation fails, empty dict if success
269
+ """
267
270
current_cmd : List [str ] = []
268
271
269
272
for token in commands :
270
273
if token == "|" :
271
274
if not current_cmd :
272
275
raise ValueError ("Empty command before pipe operator" )
273
- self ._validate_command (current_cmd )
276
+ if not self .is_command_allowed (current_cmd [0 ]):
277
+ raise ValueError (f"Command not allowed: { current_cmd [0 ]} " )
274
278
current_cmd = []
275
279
elif token in [";" , "&&" , "||" ]:
276
280
raise ValueError (f"Unexpected shell operator in pipeline: { token } " )
277
281
else :
278
282
current_cmd .append (token )
279
283
280
284
if current_cmd :
281
- self ._validate_command (current_cmd )
285
+ if not self .is_command_allowed (current_cmd [0 ]):
286
+ raise ValueError (f"Command not allowed: { current_cmd [0 ]} " )
287
+
288
+ return {}
282
289
283
290
def _split_pipe_commands (self , command : List [str ]) -> List [List [str ]]:
284
291
"""
@@ -393,33 +400,55 @@ async def execute(
393
400
"execution_time" : time .time () - start_time ,
394
401
}
395
402
396
- # Preprocess command to handle pipe operators
403
+ # Process command
397
404
preprocessed_command = self ._preprocess_command (command )
398
405
cleaned_command = self ._clean_command (preprocessed_command )
399
406
if not cleaned_command :
400
- raise ValueError ("Empty command" )
407
+ return {
408
+ "error" : "Empty command" ,
409
+ "status" : 1 ,
410
+ "stdout" : "" ,
411
+ "stderr" : "Empty command" ,
412
+ "execution_time" : time .time () - start_time ,
413
+ }
401
414
402
415
# First check for pipe operators and handle pipeline
403
416
if "|" in cleaned_command :
404
- commands : List [List [str ]] = []
405
- current_cmd : List [str ] = []
406
- for token in cleaned_command :
407
- if token == "|" :
408
- if current_cmd :
409
- commands .append (current_cmd )
410
- current_cmd = []
417
+ try :
418
+ # Validate pipeline first
419
+ error = self ._validate_pipeline (cleaned_command )
420
+ if error :
421
+ return {
422
+ ** error ,
423
+ "status" : 1 ,
424
+ "stdout" : "" ,
425
+ "execution_time" : time .time () - start_time ,
426
+ }
427
+
428
+ # Split commands
429
+ commands : List [List [str ]] = []
430
+ current_cmd : List [str ] = []
431
+ for token in cleaned_command :
432
+ if token == "|" :
433
+ if current_cmd :
434
+ commands .append (current_cmd )
435
+ current_cmd = []
436
+ else :
437
+ raise ValueError ("Empty command before pipe operator" )
411
438
else :
412
- raise ValueError ("Empty command before pipe operator" )
413
- else :
414
- current_cmd .append (token )
415
- if current_cmd :
416
- commands .append (current_cmd )
439
+ current_cmd .append (token )
440
+ if current_cmd :
441
+ commands .append (current_cmd )
417
442
418
- # Validate each command in pipeline
419
- for cmd in commands :
420
- self ._validate_command (cmd )
421
-
422
- return await self ._execute_pipeline (commands , directory , timeout )
443
+ return await self ._execute_pipeline (commands , directory , timeout )
444
+ except ValueError as e :
445
+ return {
446
+ "error" : str (e ),
447
+ "status" : 1 ,
448
+ "stdout" : "" ,
449
+ "stderr" : str (e ),
450
+ "execution_time" : time .time () - start_time ,
451
+ }
423
452
424
453
# Then check for other shell operators
425
454
for token in cleaned_command :
0 commit comments