簡體   English   中英

我如何在我的 web 應用程序上安全地接受和運行用戶代碼?

[英]How can i accept and run user's code securely on my web app?

我正在開發一個基於 django 的 web 應用程序,該應用程序將 python 文件作為輸入,其中包含一些 function,然后在后端我有一些列表作為參數通過用戶的 function 傳遞,這將生成單個值 883437810.6334 結果將是用於一些進一步的計算。

這是用戶文件中的 function 的樣子:

def somefunctionname(list):

    ''' some computation performed on list'''

    return float value

目前我使用的方法是將用戶文件作為普通文件輸入。 然后在我的 views.py 中,我將文件作為模塊執行,並使用 eval function 傳遞參數。下面給出了代碼片段。

這里的 modulename 是我從用戶那里獲取並作為模塊導入的 python 文件名

exec("import "+modulename)

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

哪個工作得很好。 但我知道這不是安全的方法。

我的問題是,由於我使用的方法不安全,是否還有其他方法可以安全地運行用戶文件? 我知道提議的解決方案不能完全證明,但我可以通過哪些其他方式運行它(比如如果它可以通過 dockerization 解決那么我可以使用 API 的方法或一些外部工具是什么)? 或者,如果可能的話,有人可以告訴我如何簡單地沙盒這個或任何可以幫助我的教程..?

任何參考或資源都會有所幫助。

這是一個重要的問題。 在 python 中,沙盒並不簡單。

這是少數幾個問題之一,您使用的是哪個版本的 python 解釋器。 例如,Jyton 生成 Java 字節碼,而 JVM 有自己的安全運行代碼的機制。

對於默認解釋器 CPython,最初有一些嘗試進行限制執行模式,但很久以前就放棄了。

目前,有一個非官方項目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運行會禁用危險功能,如打開文件、導入或其他任何功能。 還有其他內置函數和其他選項的變體,花一些時間閱讀文檔,它們非常好。

編輯:

這是您用例的示例

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

現在你有一個 function execute_user_code ,它接收一些不安全的代碼作為字符串,這個代碼的名稱 function,arguments,並返回 function 的返回值和給定的 arguments。

這是一些用戶代碼的非常愚蠢的例子:

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

可能的擴展:

請注意,每次調用 function 時都會編譯用戶代碼。但是,您可能希望編譯一次用戶代碼,然后使用不同的參數執行它。 所以你所要做的就是將字節碼保存在byte_code地方,然后每次用一組不同的restricted_locals調用 exec 。

編輯2:

如果你想使用 import,你可以寫你自己的 import function 允許只使用你認為安全的模塊。 例子:

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))

請注意,此示例 import function 非常原始,它不適用於from x import y之類的東西。 您可以在此處查看更復雜的實現。

編輯3

請注意,許多 python 內置功能在 RestrictedPython 中不是開箱即用的,這並不意味着它根本不可用。 您可能需要實施一些 function 才能使其可用。

甚至一些像sum+=運算符這樣明顯的東西在受限環境中也不明顯。

例如, for循環使用_getiter_ function,您必須自己實現並提供(在全局變量中)。 由於您希望避免無限循環,您可能希望對允許的迭代次數設置一些限制。 下面是一個將迭代次數限制為 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))

如果您不想限制循環次數,只需使用身份 function 作為_getiter_

restricted_globals = {
    "_getiter_": labmda x: x,

請注意,簡單地限制循環次數並不能保證安全。 首先,循環可以嵌套。 其次,您不能限制while循環的執行次數。 為了確保安全,您必須在超時后執行不安全的代碼。

請花點時間閱讀文檔

請注意,並非所有內容都已記錄(盡管有很多內容)。 你必須學會閱讀項目的源代碼以獲得更高級的東西。 最好的學習方法是嘗試運行一些代碼,看看缺少什么樣的 function,然后查看項目的源代碼以了解如何實現它。

編輯4

還有一個問題——受限代碼可能會無限循環。 為避免這種情況,代碼需要某種超時。

不幸的是,由於您使用的是 django,除非您明確指定,否則它是多線程的,使用 signeals 的簡單超時技巧在這里不起作用,您必須使用多處理。

在我看來最簡單的方法是使用這個庫 只需向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 攻擊。

對於 docker 可以肯定,如果你小心的話,你可以沙箱執行。 您可以限制 CPU 周期,最大 memory,關閉所有網絡端口,以對文件系統具有只讀訪問權限的用戶身份運行等等)。

盡管如此,我認為這將是非常復雜的。 對我來說,您不得允許客戶執行那樣的任意代碼。

我會檢查生產/解決方案是否尚未完成並使用它。 我在想,有些網站允許您提交一些在服務器上執行的代碼(python、java 等)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM