Skip to content

Commit 36f350d

Browse files
committed
Updated CodeSafe with new allow_attributes and set immediate_termination to be True by default for safer function calling.
1 parent 437e9b4 commit 36f350d

File tree

3 files changed

+58
-33
lines changed

3 files changed

+58
-33
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ An open-source Python library for code encryption, decryption, and safe evaluati
1212
> [!NOTE]
1313
> **CodeSafe** is intended to quickly encrypt/decrypt code files, and run them (only for Python script files) while in their encrypted form, but not as a means for powerful encryption, just code obfuscation. We have also included a `safe_eval` function, that can safely evaluate expressions within a safe environment.
1414
15+
### Changelog v0.0.3:
16+
- Added an `allow_attributes` parameter to `safe_eval` and set `immediate_termination` to be `True` by default for safer function calling.
17+
18+
### Changelog v0.0.2:
19+
- Fixed function returns.
20+
- Added error handling to `CodeSafe`, removed some print statements with edits from `@0XC7R`.
21+
22+
### Changelog v0.0.1:
23+
- Initial release
24+
1525
## Installation
1626

1727
You can install CodeSafe using pip:

codesafe/__init__.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
import ast
22
import builtins
33
from multiprocessing import Process, Queue
4+
import gc
45
import base64
56
import 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-
1610
class 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

3630
def 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)
164161
SHIFT = 3 # Caesar cipher shift value

test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ def test_custom_sum(self):
4848

4949
self.assertEqual(result, 30)
5050

51+
def test_safe_attribute_access(self):
52+
expression = "'hello'.upper()"
53+
result = safe_eval(expression, allow_attributes=True)
54+
self.assertEqual(result, "HELLO")
55+
56+
def test_unsafe_attribute_access(self):
57+
expression = "().__class__.__bases__[0]"
58+
with self.assertRaises(UnsafeExpressionError):
59+
safe_eval(expression, allow_attributes=True)
60+
61+
def test_attribute_access_disabled(self):
62+
expression = "'hello'.upper()"
63+
with self.assertRaises(UnsafeExpressionError):
64+
safe_eval(
65+
expression,
66+
allow_attributes=False # Explicitly disable
67+
)
68+
5169
def test_security_error_on_unsafe_function(self):
5270
expression = "os.getcwd()" # Attempting to call an unsafe function
5371
try:

0 commit comments

Comments
 (0)