zopefoundation / RestrictedPython

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

Single-line Python Expression Usage - question/bug #266

Closed dopc closed 8 months ago

dopc commented 8 months ago

BUG/PROBLEM REPORT / FEATURE REQUEST

Hey, thanks for this great project. I have an application which allows users to run a single Python expression. I want to restrict some imports and functions. Therefore I want to utilize RestrictedPython for that.

In my application users may use __import__ function directly. I am planning to write a custom import function. I have read about guarded_import. However, I want to try the simplest one first in the below experiment. So in this issue, I am reporting about that.

What I did:

I just tried the below code. However, it failed. It is because of that line.

from RestrictedPython import compile_restricted, safe_builtins

safe_builtins['__import__'] = __import__

source_code = "__import__('numpy').array([3,5])"

byte_code = compile_restricted(
    source_code,
    mode='eval',
)
result = eval(byte_code, {'__builtins__': safe_builtins}, None)

SyntaxError: ('Line 1: "__import__" is an invalid variable name because it starts with "_"',)

What I expect to happen:

Then I give None as the policy and it run.

from RestrictedPython import compile_restricted, safe_builtins

safe_builtins['__import__'] = __import__

source_code = "__import__('numpy').array([3,5])"

byte_code = compile_restricted(
    source_code,
    mode='eval',
    policy=None
)
result = eval(byte_code, {'__builtins__': safe_builtins}, None)

array([3, 5])

What I want to ask:

So, I want to ask about the policy and using __import__ directly like my above example.

  1. Should I write a custom policy for my case?
  2. Is using None as the policy a "bad" thing for my case?
  3. Or is there a bug or something which causes the above error?

What version of Python and Zope/Addons I am using:

ProductName:    macOS
ProductVersion: 12.5.1
BuildVersion:   21G83
Python 3.8.16
RestrictedPython==7.0
d-maurer commented 8 months ago

dopc wrote at 2024-1-3 23:40 -0800:

...

What I did:

I just tried the below code. However, it failed. It is because of that line.

from RestrictedPython import compile_restricted, safe_builtins

safe_builtins['__import__'] = __import__

source_code = "__import__('numpy').array([3,5])"

byte_code = compile_restricted(
   source_code,
   mode='eval',
)
result = eval(byte_code, {'__builtins__': safe_builtins}, None)

SyntaxError: ('Line 1: "__import__" is an invalid variable name because it starts with "_"',)

Names starting with _ are (by convention) special in Python.

RestrictedPython uses as (very deeply) built-in policy, never to allow access to a variable starting with _. As a consequence, code to be run by RestrictedPython cannot access variable starting with _ and especially __import__.

You can work around by providing an import functionality via a name not starting with _, e.g. restricted_import.

loechel commented 8 months ago

Hi @dopc

first of all, yes, RestrictedPython might be a good solution or inspiration to secure a single-line Python expression execution. My first thought by reading your code example was, why do you would like to allow an __import__(<name>) expression? It neither looks Pythonic nor readable but allow a certain import or access statement. But yes an statement would break your one-line- requirement.

I would suggest, to go with an extended set of safe_builtins or utility_builtins (see https://github.com/zopefoundation/RestrictedPython/blob/master/src/RestrictedPython/Utilities.py) and explicitly allow certain functions.

Your example could look like this:

from RestrictedPython import compile_restricted, safe_builtins
from numpy import array

safe_builtins['array'] = array

source_code = "array([3,5])"

byte_code = compile_restricted(
    source_code,
    mode='eval',
)
result = eval(byte_code, {'__builtins__': safe_builtins}, None)
dopc commented 8 months ago

@d-maurer

def guarded_import(name, *args, **kwargs):
    if name =='os': # now allow to import os
        raise RuntimeError('cannot import os')
    return __import__(name, *args, **kwargs)

from RestrictedPython import compile_restricted, safe_builtins

safe_builtins['guarded_import'] = guarded_import

source_code = "guarded_import('numpy').array([3, 5])"

byte_code = compile_restricted(
    source_code,
    mode='eval',
)
result = eval(byte_code, {'__builtins__': safe_builtins}, None)

It worked as above by using a function from #225 , thanks.


@loechel Your points about my code make sense to me. I will consider your suggestions to make my solution better. Thanks!