11import ast
22import builtins
33from multiprocessing import Process , Queue
4+ import gc
45import base64
56import re
6- import gc
77
88__all__ = ['safe_eval' , 'EvaluationTimeoutError' , 'UnsafeExpressionError' , 'encrypt_to_file' , 'encrypt' , 'decrypt' , 'run' , 'decrypt_to_file' ]
99
10- def cleanup_resources ():
11- """
12- Clean up all open resources by closing them.
13- """
14- gc .collect () # Force garbage collection
15-
1610class EvaluationTimeoutError (Exception ):
1711 """Custom exception to handle timeouts during evaluation."""
1812 def __init__ (self , error ):
@@ -34,12 +28,13 @@ def _eval_in_process(expr: str, safe_globals: dict, queue: Queue):
3428 queue .put (e )
3529
3630def safe_eval (expr : str ,
37- allowed_builtins : dict = None ,
38- allowed_vars : dict = None ,
31+ allowed_builtins : dict = {} ,
32+ allowed_vars : dict = {} ,
3933 timeout : float = 5 ,
40- restricted_imports : list = None ,
41- allowed_function_calls : list = None ,
42- immediate_termination : bool = False ,
34+ restricted_imports : list = [],
35+ allowed_function_calls : list = [],
36+ allow_attributes : bool = False ,
37+ immediate_termination : bool = True ,
4338 file_access : bool = False ,
4439 network_access : bool = False ) -> object :
4540 """
@@ -52,7 +47,8 @@ def safe_eval(expr: str,
5247 timeout (float, optional): Time limit for evaluation in seconds. Defaults to 5.
5348 restricted_imports (list, optional): A list of restricted imports or modules. Defaults to [].
5449 allowed_function_calls (list, optional): A list of allowed function names to call. Defaults to [].
55- immediate_termination (bool, optional): Whether to forcibly terminate the evaluation if it exceeds the timeout. Defaults to False.
50+ allow_attributes (bool, optional): Whether to allow access to safe attributes and methods (e.g., 'str.upper()'). Defaults to False.
51+ immediate_termination (bool, optional): Whether to forcibly terminate the evaluation if it exceeds the timeout. Defaults to True.
5652 file_access (bool, optional): Whether to allow file access (open, etc.). Defaults to False.
5753 network_access (bool, optional): Whether to allow network access (requests, etc.). Defaults to False.
5854
@@ -65,21 +61,16 @@ def safe_eval(expr: str,
6561 UnsafeExpressionError: If restricted imports or unsafe nodes are detected in the AST.
6662 SyntaxError: If the expression contains invalid syntax.
6763 """
68- allowed_builtins = allowed_builtins or {}
69- allowed_vars = allowed_vars or {}
70- restricted_imports = restricted_imports or []
71- allowed_function_calls = allowed_function_calls or []
7264
7365 # Restrict file access by removing file-related functions from built-ins if file_access is False
74- safe_builtins = allowed_builtins .copy ()
66+ safe_builtins = {k : v for k , v in builtins .__dict__ .items ()}
67+ safe_builtins .update (allowed_builtins )
68+
7569 if not file_access :
7670 # Remove all file-related functions from built-ins
77- file_access_functions = {'open' , 'os.remove' , 'os.rename' , 'os.rmdir' , 'os.unlink' ,
78- 'os.mkdir' , 'os.makedirs' , 'os.chmod' , 'os.chown' ,
79- 'os.truncate' , 'os.path' , 'shutil' , 'pathlib' , 'subprocess' }
80-
81- # Filter out functions related to file access
82- safe_builtins = {k : v for k , v in builtins .__dict__ .items () if k not in file_access_functions }
71+ file_funcs = {'open' }
72+ for func in file_funcs :
73+ safe_builtins .pop (func , None )
8374
8475 if not network_access :
8576 # Remove networking-related functions from built-ins
@@ -98,10 +89,9 @@ def safe_eval(expr: str,
9889 try :
9990 parsed_expr = ast .parse (expr , mode = 'eval' )
10091 except SyntaxError as e :
101- cleanup_resources ()
10292 raise SyntaxError (f"Invalid syntax: { e } " )
10393
104- _check_ast (parsed_expr , restricted_imports , allowed_function_calls )
94+ _check_ast (parsed_expr , restricted_imports , allowed_function_calls , allow_attributes )
10595
10696 queue = Queue ()
10797 process = Process (target = _eval_in_process , args = (expr , safe_globals , queue ))
@@ -113,36 +103,40 @@ def safe_eval(expr: str,
113103 if immediate_termination :
114104 process .terminate () # Terminate the process immediately
115105 process .join () # Ensure the process has finished
116- cleanup_resources ()
106+ gc . collect ()
117107 raise EvaluationTimeoutError ("Evaluation timed out and was terminated." )
118108 else :
119- cleanup_resources ()
109+ gc . collect ()
120110 raise EvaluationTimeoutError ("Evaluation timed out." )
121111
122112 # Check for results or exceptions
123113 if not queue .empty ():
124114 result = queue .get ()
125115 if isinstance (result , Exception ):
126- cleanup_resources ()
116+ gc . collect ()
127117 raise result
128118 else :
129- cleanup_resources ()
119+ gc . collect ()
130120 raise EvaluationTimeoutError ("No result returned from the evaluation process." )
131121
132122 return result
133123
134- def _check_ast (parsed_expr , restricted_imports , allowed_function_calls ):
124+ def _check_ast (parsed_expr , restricted_imports , allowed_function_calls , allow_attributes ):
135125 """
136126 Check the AST for unsafe operations such as imports and function calls.
137127
138128 Parameters:
139129 parsed_expr (ast.AST): The parsed AST expression.
140130 restricted_imports (list): A list of restricted import statements.
141131 allowed_function_calls (list): A list of allowed function calls.
132+ allow_attributes (bool): Whether to allow attribute access.
142133
143134 Raises:
144135 UnsafeExpressionError: If unsafe operations are detected in the expression.
145136 """
137+ # Attributes starting with '__' are always blocked if attributes are allowed.
138+ blocked_attrs = {'__globals__' , '__closure__' , '__code__' , '__subclasses__' , '__init__' , '__class__' , '__bases__' }
139+
146140 for node in ast .walk (parsed_expr ):
147141 # Block all imports if restricted
148142 if isinstance (node , (ast .Import , ast .ImportFrom )):
@@ -157,8 +151,11 @@ def _check_ast(parsed_expr, restricted_imports, allowed_function_calls):
157151 raise UnsafeExpressionError (f"Function call to '{ node .func .id } ' is not allowed." )
158152
159153 # Prevent attribute access (e.g., accessing os.system or other potentially harmful attributes)
160- if isinstance (node , ast .Attribute ):
161- raise UnsafeExpressionError ("Attribute access is restricted for security reasons." )
154+ if isinstance (node , ast .Attribute ) and not allow_attributes :
155+ raise UnsafeExpressionError ("Attribute access is disabled." )
156+ elif isinstance (node , ast .Attribute ) and allow_attributes :
157+ if node .attr .startswith ('__' ) and node .attr in blocked_attrs :
158+ raise UnsafeExpressionError (f"Access to dunder attribute '{ node .attr } ' is not allowed." )
162159
163160# Custom key (visible but obfuscated)
164161SHIFT = 3 # Caesar cipher shift value
0 commit comments