zopefoundation / RestrictedPython

A restricted execution environment for Python to run untrusted code.
http://restrictedpython.readthedocs.io/
Other
456 stars 38 forks source link

exec Function Allowed in Restricted Python Environment #284

Open Abdullahsecuriti opened 1 month ago

Abdullahsecuriti commented 1 month ago

BUG/PROBLEM REPORT / FEATURE REQUEST

Description:

In a restricted Python package environment, the following code snippet:

python 3.10.0

try:
  exec("import os; os.system('ls'); print('**')")
except:
  pass

successfully executes without throwing an error, despite exec being expected to be undefined in such a restricted environment.

However, when the code is run without the try-except block:

exec("import os; os.system('ls'); print('**')") an error is thrown as expected.

Expected Behavior:

The restricted environment should prevent the execution of exec and throw an error when it is invoked, regardless of the surrounding try-except block.

d-maurer commented 1 month ago

Abdullahsecuriti wrote at 2024-8-2 10:42 -0700:

Description:

In a restricted Python package environment, the following code snippet:

python Copy code

try:
 exec("import os; os.system('ls'); print('**')")
except:
 pass

successfully executes without throwing an error, despite exec being expected to be undefined in such a restricted environment.

exec is undefined (if you do everything right, see below) and the access raises a NameError which you ignore with your try: ... except: ....

However, when the code is run without the try-except block:

python Copy code exec("import os; os.system('ls'); print('**')") an error is thrown as expected.

You get the exception in this case because you do not ignore exceptions here.

Expected Behavior:

The restricted environment should prevent the execution of exec and throw an error when it is invoked, regardless of the surrounding try-except block.

Your expectation is wrong: (unlike for Python 2) exec is a function (in Python 3) and RestrictedPython has no need to treat is specially. When you execute restricted code, you typically supply an execution environment (via the globals parameter of exec) with a limited builtins set (especially not containing exec). If your restricted code references exec (during execution) and exec is not in the execution environment, you will get a NameError (which might get try: ... except:ed).

For your future problem reports: please provide FULL code snippets reproducing the problem. In your current report, you supply the restricted code but not how it is compiled nor in what execution environment it is executed. The code you provide should allow to run it (after extraction) and then reproduce the problem.

Abdullahsecuriti commented 1 month ago

@d-maurer Thank you for your quick response; it's highly appreciated.

I have successfully executed the following code:

async def __main():
    # Import necessary modules from RestrictedPython
    import textwrap
    from RestrictedPython import compile_restricted, safe_builtins
    from RestrictedPython.Utilities import utility_builtins
    from RestrictedPython.Eval import default_guarded_getattr

    def execute_restricted_code(user_code):
        try:
            # Format the user-provided code
            formatted_code = textwrap.dedent(user_code)
            # Define safe globals for restricted code execution
            safe_globals = {
                '__builtins__': safe_builtins,  # built-in policy allowing basic Python functions
            }

            # Compile the normalized and formatted user-provided code into a restricted code object
            restricted_code = compile(formatted_code, '<usercode>', 'exec')

            # Create a list to capture printed output
            captured_output = []

            # Define a custom print function to capture output
            def safe_print(*args, **kwargs):
                output = ' '.join(str(arg) for arg in args)
                captured_output.append(output)

            # Execute the restricted code within the restricted context
            exec(restricted_code, safe_globals, {'print': safe_print})

            # Print the captured output directly within the function
            for line in captured_output:
                print(line)

            return '\n'.join(captured_output), None

        except Exception as e:
            error_message = f"Error executing restricted code: {e}"
            print(error_message)
            return None, error_message

    # This is User Input from Node
    user_code = '''
    def restrictedPythonMain():
        try:
            # Attempt to use a restricted operation (e.g., system command or file access)
            restricted_operation = lambda: exec("print('hello world')")
            restricted_operation()
        except:
            # Catch any exceptions that indicate unauthorized access is prevented
            pass
    restrictedPythonMain()
    '''

    result, error_message = execute_restricted_code(user_code)

    if result is None:
        errorHandler = [{'error': str(error_message)}]
        return errorHandler

await __main()

While this code executes successfully, it allows the use of the exec function, which is not intended. I observed the output "hello world" on the console, indicating that restricted functions were not blocked as expected.

However, when the user_code is as follows, an error is thrown due to the use of the exec function, which is the desired behavior:

user_code = '''
def restrictedPythonMain():
    exec("print('hello world')")
restrictedPythonMain()
'''

Could you please advise on what might be incorrect in my implementation? I aim to prevent users from calling functions like exec in any context within the restricted environment.

Thank you for your assistance.

d-maurer commented 1 month ago

Abdullahsecuriti wrote at 2024-8-3 05:40 -0700:

I have successfully executed the following code:

async def __main():

Import necessary modules from RestrictedPython

import textwrap from RestrictedPython import compile_restricted, safe_builtins ... def execute_restricted_code(user_code): try:

Format the user-provided code

       formatted_code = textwrap.dedent(user_code)
       # Define safe globals for restricted code execution
       safe_globals = {
           '__builtins__': safe_builtins,  # built-in policy allowing basic Python functions
       }

       # Compile the normalized and formatted user-provided code into a restricted code object
       restricted_code = compile(formatted_code, '<usercode>', 'exec')

Why are you using compile here (instead of compile_restricted)? With compile you get unrestricted code (likely not intended as you assign it to a variable named restricted_code).

... exec(restricted_code, safe_globals, {'print': safe_print})

Even though your code is unrestricted, you should still get a BameError: nameexecis not defined because you execution environment does not contain exec.

For a verification, I simplified your (overly complex) code:

from RestrictedPython import compile_restricted, safe_builtins

safe_globals = {'__builtins__': safe_builtins, 'print': print}
src = """
def f():
  exec("print('hello')")
f()
"""
code= compile(src, "usercode", "exec")
ld = {}
exec(code, safe_globals, ld)

It raises the expected exception.

I suggest you replace compile by compile_restricted (to get restricted code) and then start from my example code to reproduce the problem. Keep it as simple as possible (but still showing the problem). Avoid artificial function definitions (e.g. lambdas).