RestrictedPython: Вызывать другие функции внутри пользовательского кода?
Использование кода Юрия Нудельмана с пользовательским определением _import для указания модулей для ограничения служит хорошей основой, но при вызове функций внутри этого пользовательского_кода, естественно, из-за необходимости составлять белый список всего, есть ли способ разрешить вызов других функций, определенных пользователем? Я открыт для других решений по созданию песочницы, хотя Jupyter не кажется простым для встраивания в веб-интерфейс.
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))
Запуск этого возвращает, что 'foo' не определен
В exec(byte_code, restricted_globals, restricted_locals):
def foo():изменяетrestricted_locals.myceilможет видеть только свои глобальные файлы, т.е.myceil.__globals__, который являетсяrestricted_globals.
Вы можете обновить f.__globals__/restricted_globals в _apply:
def _apply(f, *a, **kw):
for k, v in restricted_locals.items():
if k not in _restricted_locals_keys and k not in f.__globals__:
f.__globals__[k] = v
return f(*a, **kw)
где _restricted_locals_keys есть:
restricted_locals = {
"result": None,
"args": args,
"kwargs": kwargs,
}
_restricted_locals_keys = set(restricted_locals.keys())
Если вы не хотите изменять restricted_globals, то скопируйте f с новыми глобалами в _apply:
import copy
import types
def _copy_func(f, func_globals=None):
func_globals = func_globals or f.__globals__
return types.FunctionType(f.__code__, func_globals, name=f.__name__, argdefs=f.__defaults__, closure=f.__closure__)
def _apply(f, *a, **kw):
func_globals = copy.copy(f.__globals__)
for k, v in restricted_locals.items():
if k not in _restricted_locals_keys and k not in func_globals:
func_globals[k] = v
f = _copy_func(f, func_globals=func_globals)
return f(*a, **kw)
Прежде всего, замена встроенного модуля на __import__ реализована неправильно. Эта встроенная функция должна возвращатьимпортированный модуль, а не изменять глобальные файлы, чтобы включить его:
Python 3.9.12 (main, Mar 24 2022, 13:02:21)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> __import__('math')
<module 'math' (built-in)>
>>> math
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'math' is not defined
Более эффективным способом реализации __import__ было бы следующее:
_SAFE_MODULES = frozenset(("math",))
def _safe_import(name, *args, **kwargs):
if name not in _SAFE_MODULES:
raise Exception(f"Don't you even think about {name!r}")
return __import__(name, *args, **kwargs)
Тот факт, что вы мутировали глобальные объекты в вашей первоначальной реализации, частично маскировал основную ошибку. А именно: присвоения имен в ограниченном коде (определения функций, присвоения переменных и импорты) мутируют дикт локалей, но присвоения имен по умолчанию выполняются как глобальные, минуя локали. Вы можете убедиться в этом, дизассемблировав ограниченный байткод с помощью :__import__('dis').dis(byte_code)
2 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (math)
6 STORE_NAME 0 (math)
4 8 LOAD_CONST 2 (<code object foo at 0x7fbef4eef3a0, file "<user_code>", line 4>)
10 LOAD_CONST 3 ('foo')
12 MAKE_FUNCTION 0
14 STORE_NAME 1 (foo)
7 16 LOAD_CONST 4 (<code object myceil at 0x7fbef4eef660, file "<user_code>", line 7>)
18 LOAD_CONST 5 ('myceil')
20 MAKE_FUNCTION 0
22 STORE_NAME 2 (myceil)
24 LOAD_CONST 1 (None)
26 RETURN_VALUE
Disassembly of <code object foo at 0x7fbef4eef3a0, file "<user_code>", line 4>:
5 0 LOAD_CONST 1 (7)
2 RETURN_VALUE
Disassembly of <code object myceil at 0x7fbef4eef660, file "<user_code>", line 7>:
8 0 LOAD_GLOBAL 0 (_getattr_)
2 LOAD_GLOBAL 1 (math)
4 LOAD_CONST 1 ('ceil')
6 CALL_FUNCTION 2
8 LOAD_FAST 0 (x)
10 CALL_FUNCTION 1
12 LOAD_GLOBAL 2 (foo)
14 CALL_FUNCTION 0
16 BINARY_ADD
18 RETURN_VALUE
Раздельные отображения для локалов и глобалов совершенно надуманны, поскольку документация для exec объясняет (выделено мной):
Если указано только globals, то это должен быть словарь (а не подкласс словаря), который будет использоваться как для глобальных, так и для локальных переменных. Если указаны globals и locals, то они используются для глобальных и локальных переменных, соответственно. Если указано, locals может быть любым объектом отображения. Помните, что на уровне модуля globals и locals - это один и тот же словарь. Если exec получает два отдельных объекта как globals и locals, код будет выполнен, как если бы он был встроен в определение класса.
Поэтому мы можем просто избавиться от диктанта locals и поместить все в globals. Весь код должен выглядеть примерно так:
from RestrictedPython import safe_builtins, compile_restricted
_SAFE_MODULES = frozenset(("math",))
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))
def execute_user_code(user_code, user_func, *args, **kwargs):
my_globals = {
"__builtins__": {
**safe_builtins,
"__import__": _safe_import,
},
}
try:
byte_code = compile_restricted(
user_code, filename="<user_code>", mode="exec")
except SyntaxError:
# syntax error in the sandboxed code
raise
try:
exec(byte_code, my_globals)
return my_globals[user_func](*args, **kwargs)
except BaseException:
# runtime error (probably) in the sandboxed code
raise
Выше мне также удалось исправить пару касательных вопросов:
- Вместо того, чтобы вставлять вызов функции в скомпилированный фрагмент, я ищу функцию в дикте globals напрямую. Это позволяет избежать потенциального вектора инъекции кода, если
user_funcпридет из недоверенного источника, и избежать необходимости инъекцииargs,kwargsиresultв песочницу, что позволит коду песочницы разрушить ее. - Я избегаю мутировать объект
safe_builtins, предоставляемый модулемRestrictedPython. В противном случае, если какой-либо другой код в вашей программе случайно используетRestrictedPython, он может быть затронут. - Я разделил обработку исключений между двумя этапами: компиляцией и выполнением. Это минимизирует вероятность того, что ошибки в коде песочницы будут ошибочно отнесены к коду песочницы. .
- Я изменил тип перехватываемого исключения времени выполнения на
BaseException, чтобы также перехватывать случаи, когда код песочницы пытается поднятьKeyboardInterruptилиSystemExit(которые не являются производными отException, а толькоBaseException). - Я также удалил ссылки на
_getitem_и_apply_, которые, похоже, ни для чего не используются. Если окажется, что они все-таки нужны, вы можете восстановить их.
(Обратите внимание, однако, что это не защищает от DoS через бесконечные циклы в песочнице)