[英]How can I get the user's IP-Address in my Cloud-Run Flask app?
[英]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.