Using Yuri Nudelman's code with the custom _import definition to specify modules to restrict serves as a good base but when calling functions within said user_code naturally due to having to whitelist everything is there any way to permit other user defined functions to be called? Open to other sandboxing solutions although Jupyter didn't seem straight-forward to embed within a web interface.
from RestrictedPython import safe_builtins, compile_restricted
from RestrictedPython.Eval import default_guarded_getitem
def _import(name, globals=None, locals=None, fromlist=(), level=0):
safe_modules = ["math"]
if name in safe_modules:
globals[name] = __import__(name, globals, locals, fromlist, level)
else:
raise Exception("Don't you even think about it {0}".format(name))
safe_builtins['__import__'] = _import # Must be a part of builtins
def execute_user_code(user_code, user_func, *args, **kwargs):
""" Executed user code in restricted env
Args:
user_code(str) - String containing the unsafe code
user_func(str) - Function inside user_code to execute and return value
*args, **kwargs - arguments passed to the user function
Return:
Return value of the user_func
"""
def _apply(f, *a, **kw):
return f(*a, **kw)
try:
# This is the variables we allow user code to see. @result will contain return value.
restricted_locals = {
"result": None,
"args": args,
"kwargs": kwargs,
}
# If you want the user to be able to use some of your functions inside his code,
# you should add this function to this dictionary.
# By default many standard actions are disabled. Here I add _apply_ to be able to access
# args and kwargs and _getitem_ to be able to use arrays. Just think before you add
# something else. I am not saying you shouldn't do it. You should understand what you
# are doing thats all.
restricted_globals = {
"__builtins__": safe_builtins,
"_getitem_": default_guarded_getitem,
"_apply_": _apply,
}
# Add another line to user code that executes @user_func
user_code += "\nresult = {0}(*args, **kwargs)".format(user_func)
# Compile the user code
byte_code = compile_restricted(user_code, filename="<user_code>", mode="exec")
# Run it
exec(byte_code, restricted_globals, restricted_locals)
# User code has modified result inside restricted_locals. Return it.
return restricted_locals["result"]
except SyntaxError as e:
# Do whaever you want if the user has code that does not compile
raise
except Exception as e:
# The code did something that is not allowed. Add some nasty punishment to the user here.
raise
i_example = """
import math
def foo():
return 7
def myceil(x):
return math.ceil(x)+foo()
"""
print(execute_user_code(i_example, "myceil", 1.5))
Running this returns 'foo' is not defined
First of all, the replacement for the
__import__built-in is implemented incorrectly. That built-in is supposed to return the imported module, not mutate the globals to include it:A better way to reimplement
__import__would be this:The fact that you mutated globals in your original implementation was partially masking the primary bug. Namely: name assignments within restricted code (function definitions, variable assignments and imports) mutate the locals dict, but name look-ups are by default done as global look-ups, bypassing the locals entirely. You can see this by disassembling the restricted bytecode using
__import__('dis').dis(byte_code):The documentation for
execexplains (emphasis mine):This makes separate mappings for locals and globals completely spurious. We can therefore simply get rid of the locals dict, and put everything in globals. The entire code should look something like this:
Above I also managed to fix a couple of tangential issues:
user_funchappens to come from an untrusted source, and avoids having to injectargs,kwargsandresultinto the sandbox, which would enable sandboxed code to clobber it.safe_builtinsobject provided by theRestrictedPythonmodule. Otherwise, if any other code within your program happens to be usingRestrictedPython, it might have been affected.BaseException, to also catch cases when sandboxed code attempts to raiseKeyboardInterruptorSystemExit(which do not derive fromException, but onlyBaseException)._getitem_and_apply_, which don’t seem to be used for anything. If they turn out to be necessary after all, you may restore them.(Note, however, that this still does not protect against DoS via infinite loops within the sandbox.)