【问题标题】:How can i accept and run user's code securely on my web app?如何在我的网络应用程序上安全地接受和运行用户的代码?
【发布时间】:2020-11-19 10:38:15
【问题描述】:

我正在开发一个基于 django 的网络应用程序,该应用程序将 python 文件作为输入,其中包含一些函数,然后在后端我有一些列表作为参数通过用户函数传递,这将生成单个值输出。结果生成的将用于进一步的计算。

用户文件中的函数如下所示:

def somefunctionname(list):

    ''' some computation performed on list'''

    return float value

目前我使用的方法是将用户文件作为普通文件输入。然后在我的views.py中,我将文件作为模块执行并使用eval函数传递参数。片段如下。

这里的 modulename 是我从用户那里获取并作为模块导入的 python 文件名

exec("import "+modulename)

result = eval(f"{modulename}.{somefunctionname}(arguments)")

它工作得非常好。但我知道这不是安全的方法。

我的问题,由于我使用的方法不安全,是否有任何其他方法可以安全地运行用户文件?我知道提出的解决方案不能完全证明,但是我可以通过哪些其他方式运行它(例如,如果它可以通过 dockerization 解决,那么我可以使用 API 的方法或一些外部工具是什么)? 或者如果可能的话,有人可以告诉我如何简单地将这个或任何可以帮助我的教程沙箱化..?

任何参考资料或资源都会有所帮助。

【问题讨论】:

    标签: python python-3.x django docker google-kubernetes-engine


    【解决方案1】:

    这是一个重要的问题。在 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​​ 的攻击。

    【讨论】:

    • 如果你关心的是性能,我可以保证它不会比使用 eval 差。另外,在你编译了一些字节码之后,你可以重用它的函数,就像这里的基本示例一样restrictedpython.readthedocs.io/en/latest/usage/…
    • 几乎所有内置函数 - sum、min、max、str,都不会在受限代码中定义。如果你想允许用户使用一个特定的函数,你必须在restricted_globals 中传递它。例如 sum,你必须通过 "sum" : sum。无论您明确不允许 - 用户都不能使用。这就是将这段代码变成受限代码的原因。
    • 是的,解决无限循环是一个全新的故事,受限代码在这里没有帮助。主要挑战是受限代码仍然持有 GIL,所以你不能在不终止整个进程的情况下停止它。所以它可能必须在另一个进程中运行才能终止它。现在这是可行的,但它的资源相对繁重,这样 DDOS 攻击成为一个严重的问题,可以立即耗尽你的 CPU 资源。您可能希望严格限制允许调用此 api 的次数。但这无关紧要,如果您对此有疑问,请打开另一个问题。
    • 捕捉异常并用它做你想做的事。您可能在视图处理函数中调用execute_user_code,只需将该调用包装在try...except 块中。如果没有引发异常,则呈现一页,如果引发异常 - 呈现另一页。关于不同的参数 - 将byte_code 保存在某处。然后调用exec(byte_code, globals, locals)来执行它。 Locals 是一个包含 args 和 kwargs 的字典。在本地变量中放入一些不同的值 - exec 将返回不同的值。您可以在同一个byte_code 上多次调用exec,而无需重新编译。
    • while循环和条件语句应该没有问题。 For 循环有点不同,请参阅我上次的编辑。
    【解决方案2】:

    如果你小心的话,你可以使用 docker 来沙箱执行。您可以限制 CPU 周期、最大内存、关闭所有网络端口、以对文件系统具有只读访问权限的用户身份运行等等。

    不过,我认为这将是非常复杂的。对我来说,您不允许客户执行这样的任意代码。

    我会检查生产/解决方案是否尚未完成并使用它。我在想某些网站允许您提交一些在服务器上执行的代码(python、java 等)。

    【讨论】:

    • 是的,使用 docker 会非常复杂。此外,我对所有这些容器化仍然陌生。该项目处于开发阶段,所以我还没有决定我应该继续使用哪种解决方案。是的,有些网站允许我们执行代码,但我必须运行用户函数大约 1000 次,我有 1000 个列表要在用户函数上运行,并且站点有一些限制,比如说只有大约 60 个调用。但是,如果您在那里找到一些可以帮助我改善病情的网站,请告诉我。谢谢
    • Docker 100% 绝对不会执行不受信任的代码,即使您在锁定网络、不以 root 身份运行、更新足够频繁以至于没有任何已知的容器化的情况下“做对了”突破性漏洞,以及所有的爵士乐。
    • 那么我能想出的解决方案是什么(至少对于初始启动而言)?
    • Docker 仍然是一个可行的起点。我结合使用 Docker 映像、静态分析和看门狗。看门狗检查数据库是否有任何未决的执行,抓取下一个并启动 Docker 映像;如果容器花费的时间超过 X 秒,它会手动将其关闭。图像运行静态分析,然后是代码测试文件来处理结果,然后将它们存储在数据库中以供检索。它仍然存在一些漏洞,但这是一个很好的开始,我的团队目前正在研究。
    • 遗憾的是还没有,构建仍处于起步阶段。但是,我会说要深入阅读我的旧 SO post on unit testing python,然后深入研究让 Docker 映像运行它。基本上,从假设代码不是恶意的开始,理解这一点,然后开始考虑如何防范潜在的漏洞。
    猜你喜欢
    • 1970-01-01
    • 2013-04-10
    • 1970-01-01
    • 2013-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-12
    相关资源
    最近更新 更多