这是一个重要的问题。在 python 中,沙盒并非易事。
这是您使用哪个版本的 python 解释器的少数情况之一。例如,Jyton 生成 Java 字节码,JVM 有自己的机制来安全运行代码。
对于默认解释器CPython,最初有一些尝试制作restricted execution mode,但很久以前就被放弃了。
目前,有一个非官方项目RestrictedPython 可能会满足您的需求。它不是一个完整的沙盒,即不会给你限制文件系统访问或其他东西,但对于你的需要它可能就足够了。
基本上那里的人只是以更受限制的方式重写了 python 编译。
它允许做的是编译一段代码然后执行,所有这些都在受限模式下。例如:
from RestrictedPython import safe_builtins, compile_restricted
source_code = """
print('Hello world, but secure')
"""
byte_code = compile_restricted(
source_code,
filename='<string>',
mode='exec'
)
exec(byte_code, {__builtins__ = safe_builtins})
>>> Hello world, but secure
使用 builtins = safe_builtins 运行会禁用打开文件、导入等危险功能。 builtins 和其他选项还有其他变体,请花一些时间阅读文档,它们非常好。
编辑:
这里是你的用例示例
from RestrictedPython import safe_builtins, compile_restricted
from RestrictedPython.Eval import default_guarded_getitem
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
现在你有了一个函数execute_user_code,它接收一些不安全的字符串形式的代码、来自该代码的函数名称、参数,并返回带有给定参数的函数的返回值。
这是一些用户代码的一个非常愚蠢的例子:
example = """
def test(x, name="Johny"):
return name + " likes " + str(x*x)
"""
# Lets see how this works
print(execute_user_code(example, "test", 5))
# Result: Johny likes 25
但是当用户代码试图做一些不安全的事情时会发生这种情况:
malicious_example = """
import sys
print("Now I have the access to your system, muhahahaha")
"""
# Lets see how this works
print(execute_user_code(malicious_example, "test", 5))
# Result - evil plan failed:
# Traceback (most recent call last):
# File "restr.py", line 69, in <module>
# print(execute_user_code(malitious_example, "test", 5))
# File "restr.py", line 45, in execute_user_code
# exec(byte_code, restricted_globals, restricted_locals)
# File "<user_code>", line 2, in <module>
#ImportError: __import__ not found
可能的扩展:
请注意,每次调用函数时都会编译用户代码。但是,您可能希望编译一次用户代码,然后使用不同的参数执行它。因此,您所要做的就是将byte_code 保存在某处,然后每次使用不同的restricted_locals 集合调用exec。
EDIT2:
如果你想使用导入,你可以编写自己的导入函数,只允许使用你认为安全的模块。示例:
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
restricted_globals = {
"__builtins__": safe_builtins,
"_getitem_": default_guarded_getitem,
"_apply_": _apply,
}
....
i_example = """
import math
def myceil(x):
return math.ceil(x)
"""
print(execute_user_code(i_example, "myceil", 1.5))
请注意,这个示例导入函数非常原始,它不适用于from x import y 之类的东西。您可以查看here 以获得更复杂的实现。
EDIT3
请注意,许多 Python 内置功能在 RestrictedPython 中开箱即用不可用,这并不意味着它根本不可用。您可能需要实现一些功能才能使其可用。
即使是sum 或+= 运算符之类的明显内容,在受限环境中也不明显。
例如,for 循环使用您必须自己实现和提供的_getiter_ 函数(在全局中)。由于您想避免无限循环,您可能希望对允许的迭代次数进行一些限制。这是一个将迭代次数限制为 100 的示例实现:
MAX_ITER_LEN = 100
class MaxCountIter:
def __init__(self, dataset, max_count):
self.i = iter(dataset)
self.left = max_count
def __iter__(self):
return self
def __next__(self):
if self.left > 0:
self.left -= 1
return next(self.i)
else:
raise StopIteration()
def _getiter(ob):
return MaxCountIter(ob, MAX_ITER_LEN)
....
restricted_globals = {
"_getiter_": _getiter,
....
for_ex = """
def sum(x):
y = 0
for i in range(x):
y = y + i
return y
"""
print(execute_user_code(for_ex, "sum", 6))
如果您不想限制循环次数,只需使用身份函数作为_getiter_:
restricted_globals = {
"_getiter_": labmda x: x,
请注意,简单地限制循环次数并不能保证安全。首先,循环可以嵌套。其次,您不能限制while 循环的执行次数。为了使其安全,您必须在某个超时时间内执行不安全的代码。
请花点时间阅读docs。
请注意,并非所有内容都已记录在案(尽管很多内容都已记录)。你必须学会阅读项目的source code 以获得更高级的东西。最好的学习方法是尝试运行一些代码,看看缺少什么样的功能,然后查看项目的源代码来了解如何实现它。
EDIT4
还有一个问题——受限代码可能有无限循环。为了避免这种情况,代码需要某种超时。
不幸的是,由于您使用的是 django,它是多线程的,除非您明确指定,否则使用信号的简单超时技巧在这里不起作用,您必须使用多处理。
我认为最简单的方法 - 使用 this library。只需向execute_user_code 添加一个装饰器,它就会如下所示:
@timeout_decorator.timeout(5, use_signals=False)
def execute_user_code(user_code, user_func, *args, **kwargs):
你就完成了。代码永远不会运行超过 5 秒。
注意use_signals=False,没有这个可能会在django中出现一些意想不到的行为。
还要注意,这对资源的消耗相对较大(我真的没有办法克服这个问题)。我的意思不是真的很重,但它是一个额外的进程产生。您应该在 Web 服务器配置中牢记这一点 - 允许执行任意用户代码的 api 更容易受到 ddos 的攻击。