如何在纯Python中对Python进行沙盒化?

80

我正在纯Python中开发网络游戏,并希望有一些简单的脚本,以便能够实现更加动态的游戏内容。特权用户可以随时添加游戏内容。

如果脚本语言是Python就太好不过了,但它不能访问游戏运行环境,因为恶意用户可能会造成破坏,这是不好的。是否可以在纯Python中运行受限制的Python脚本?

更新:事实上,由于真实的Python支持将会过度臃肿,一个具有Pythonic语法的简单脚本语言将会非常完美。

如果没有Pythonic的脚本解释器,是否有其他用纯Python编写的开源脚本解释器可以使用?要求支持变量、基本条件和函数调用(不包括定义)。


如何安全地运行不受信任的Python脚本(即沙盒) - Trenton McKinney
7个回答

62

这真的很不容易。

有两种方法可以对Python进行沙盒化。一种是创建一个受限环境(即非常少的全局变量等)并在该环境中执行您的代码。这就是Messa所建议的。这很好,但有许多方法可以打破沙箱并制造麻烦。大约一年前,在Python-dev上有一个关于此问题的线程,人们从捕获异常和戳内部状态到破坏字节码操作做了很多事情。如果您想要完整的语言,则选择这种方式。

另一种方法是解析代码,然后使用ast模块来剔除您不想要的结构(例如导入语句、函数调用等),然后编译其余部分。如果您想将Python用作配置语言等,则选择这种方式。

另一种方式(可能对您无效,因为您正在使用GAE)是PyPy沙盒。虽然我自己没有使用过它,但是网络上的消息是它是唯一的真正的沙盒化Python。

根据您对要求的描述(需支持变量、基本条件和函数调用(不包括定义)),您可能希望考虑第二种方法,并将代码中的其他所有内容剔除。这有点棘手,但可行。


1
嗯,我在想如果你开始挖掘代码对象会发生什么... 我猜你可以通过这种方式逃避 exec ...不过 Google App Engine 已经在使用 PyPy 了,是吗?我想知道纯 Python 版本的 PyPy 是否可以在 GAE 上运行... 我会花点时间研究一下。 - Blixt
2
我认为GAE有一种未经过PyPY确认的变体。 - Noufal Ibrahim
1
你认为这段代码是一个好的起点吗?http://code.activestate.com/recipes/496746/ - Blixt
1
我不能给你绝对的保证,但是初步看来它是不错的代码。我知道一个在“生产环境”中这样做的地方是Templetor模板引擎,它被web.py使用。你可能想看一下它。 - Noufal Ibrahim
1
@Blixt:他们一直使用CPython。2.5.2的机制完全是用纯Python完成的。对于2.7.5,他们为ɴaᴄl‑glibc编译了Python:这是一个在C级别运行的沙盒。 - user2284570
显示剩余3条评论

16
大约在原问题提出十年之后,Python 3.8.0 推出了审计功能。它有助于解决问题吗?为简单起见,让我们将讨论限制在硬盘写入方面并进行探讨:
from sys import addaudithook
def block_mischief(event,arg):
    if 'WRITE_LOCK' in globals() and ((event=='open' and arg[1]!='r') 
            or event.split('.')[0] in ['subprocess', 'os', 'shutil', 'winreg']): raise IOError('file write forbidden')

addaudithook(block_mischief)

到目前为止,exec 可以轻松地将内容写入磁盘:

exec("open('/tmp/FILE','w').write('pwned by l33t h4xx0rz')", dict(locals()))

但是我们可以随意禁止它,这样没有恶意用户可以从exec()提供的代码访问磁盘。像numpypickle这样的Python程序库最终会使用Python的文件访问,因此它们也被禁止进行磁盘写入。外部程序调用也已被明确禁用。

WRITE_LOCK = True
exec("open('/tmp/FILE','w').write('pwned by l33t h4xx0rz')", dict(locals()))
exec("open('/tmp/FILE','a').write('pwned by l33t h4xx0rz')", dict(locals()))
exec("numpy.savetxt('/tmp/FILE', numpy.eye(3))", dict(locals()))
exec("import subprocess; subprocess.call('echo PWNED >> /tmp/FILE', shell=True)",     dict(locals()))
尝试在exec()内部删除锁定似乎是徒劳无功的,因为审计钩子使用的是不可访问exec运行的代码的不同locals副本。请证明我错了。

exec()内部尝试删除锁定似乎是徒劳无功的,因为审计钩子使用的是不可访问exec运行的代码的不同locals副本。请证明我错了。

exec("print('muhehehe'); del WRITE_LOCK; open('/tmp/FILE','w')", dict(locals()))
...
OSError: file write forbidden

当然,顶层代码可以重新启用文件I/O。

del WRITE_LOCK
exec("open('/tmp/FILE','w')", dict(locals()))

在Cpython内部进行沙箱隔离一直非常困难,许多先前的尝试都失败了。这种方法也并不完全安全,例如对于公共网络访问:

  1. 也许使用直接操作系统调用的假设编译模块无法由Cpython审核 - 建议白名单安全的纯Python模块。

  2. Cpython解释器仍然可能崩溃或过载。

  3. 也许还存在某些漏洞可以将文件写入硬盘。但我没有使用任何通常的沙箱逃避技巧来写入单个字节。我们可以说Python生态系统的“攻击面”缩小到了一个相当狭窄的(被禁止/允许)事件列表:https://docs.python.org/3/library/audit_events.html

我会感激任何指出这种方法缺陷的人。


编辑:所以这也是不安全的! 我非常感谢@Emu使用异常捕获和内省的聪明黑客攻击:

#!/usr/bin/python3.8
from sys import addaudithook
def block_mischief(event,arg):
    if 'WRITE_LOCK' in globals() and ((event=='open' and arg[1]!='r') or event.split('.')[0] in ['subprocess', 'os', 'shutil', 'winreg']):
        raise IOError('file write forbidden')

addaudithook(block_mischief)
WRITE_LOCK = True
exec("""
import sys
def r(a, b):
    try:
        raise Exception()
    except:
        del sys.exc_info()[2].tb_frame.f_back.f_globals['WRITE_LOCK']
import sys
w = type('evil',(object,),{'__ne__':r})()
sys.audit('open', None, w)
open('/tmp/FILE','w').write('pwned by l33t h4xx0rz')""", dict(locals()))

我认为审计和子处理是正确的方法,但不要在生产机器上使用:

https://bitbucket.org/fdominec/experimental_sandbox_in_cpython38/src/master/sandbox_experiment.py


3
请注意,审计钩子主要用于收集有关Python或使用Python编写的库的内部或其他不可观察操作的信息。它们不适合实现“沙盒”。特别是,恶意代码可以轻松地禁用或绕过使用此函数添加的钩子。至少,必须在初始化运行时之前使用C API PySys_AddAuditHook()添加任何安全敏感的钩子,并且应完全删除或密切监视允许任意内存修改(例如ctypes)的任何模块。 - Bazyli Debowski

11

据我所知,可以在完全隔离的环境中运行代码:

exec somePythonCode in {'__builtins__': {}}, {}

但是在这样的环境下,你几乎什么都做不了 :) (你甚至无法导入一个模块; 但是一个恶意用户仍然可以运行无限递归或导致内存耗尽)。可能你希望添加一些模块,作为与游戏引擎的接口。


1
哦,有趣。我会试一下!由于所有的代码已经被隔离在系统之外了(我正在开发GAE),所以我可以检测到无限递归/大量内存使用,并阻止脚本再次运行。 - Blixt
那很聪明。这绝对安全吗? - Ali
27
不是很准确,请尝试运行 exec [ i for i in ().__class__.__base__.__subclasses__() if i.__name__ == 'code'][0](0, 5, 8, 0, 'hello world', (), (), (), '', '', 0, '') - Michał Zieliński
@MichałZieliński,你能解释一下为什么这会导致段错误吗?我理解你创建代码对象的部分,但不知道这些参数的含义。 - Christian Oudard
13
@ChristianOudard 请查看http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html。 - Hernan
@Messa: 这不应该阻止精心制作的代码对象。 - user2284570

10
我不确定为什么没有人提到这一点,但是Zope 2有一个叫做Python Script的东西,它就是那个东西——在沙盒中执行受限制的Python,没有访问文件系统的权限,可以通过Zope安全机制控制访问其他Zope对象,导入也被限制为安全子集。
总的来说,Zope非常安全,所以我想可能没有已知或明显的方法可以突破沙盒。我不确定Python Scripts是如何实现的,但是这个功能大约从2000年左右就存在了。
这里是PythonScripts背后的魔法,附有详细文档:http://pypi.python.org/pypi/RestrictedPython,看起来甚至不依赖于Zope,因此可以作为独立项使用。
请注意,这不是为了安全地运行任意Python代码(大多数随机脚本将在第一次导入或访问文件时失败),而是为了在Python应用程序中使用Python进行有限制的脚本编写。 这个答案来自我对一个问题的评论,该问题被关闭为此重复的问题:Python from Python:限制功能?

RestrictedPython的最新版本仅与Python 2.3、2.4、2.5、2.6和2.7兼容,暂不支持Python 3。 - colidyre
1
看起来它支持Python 3.6、3.7和3.8 https://restrictedpython.readthedocs.io/en/latest/index.html - Guy Korland
1
这个解决方案值得更多的点赞。 - fbmd

3
我建议采用双服务器方法。第一台服务器是特权Web服务器,您的代码存储在其中。第二台服务器是一个非常严格受控的服务器,仅提供Web服务或RPC服务,并运行不受信任的代码。您可以为内容创建者提供自定义接口。例如,如果您允许最终用户创建项目,则可以使用查找调用带有要执行的代码和参数集的服务器。
以下是治疗药水的抽象示例。
{function_id='healing potion', action='use', target='self', inventory_id='1234'}

响应可能类似于:
{hp='+5' action={destroy_inventory_item, inventory_id='1234'}}

是的,我的游戏已经有了一个RPC API,我只想让某些事件在玩家游戏时更加动态化...所以脚本编写似乎是一个自然的选择 :) 我猜最坏的情况是我不得不自己制作一个简单的解释器。 - Blixt
您不一定需要创建复杂的API。您可以做一些简单的事情,比如序列化数据结构并将其传递给RPC服务器(运行Python),该服务器会加载结构并运行最终用户代码(Python)。最终用户对其进行修改并发送回来。无论如何,您都需要制定访问数据的准则。 - Philip Tinney
在我看来,这是最好的方法,因为它将问题简化为应用程序引擎的沙盒能力。最坏的情况下,代码可能会破坏仅运行Python代码的虚拟应用程序中的数据。我甚至认为你不需要为该应用程序使用任何持久数据。 - Ali
这真的是一个没有回答的问题。"严格控制"是什么意思?你必须选择一种沙盒技术来限制该服务器上的访问。 - Glyph
@Glyph 这真的取决于操作系统,它可能是一个chroot监狱。我想我会让实现者自己找出适合他们的方法。个人而言,由于很有可能会错过某些东西并留下一个大洞,我会谨慎使用任何提供的解析和编译解决方案。以rexec和bastion的问题为例。考虑到http://wiki.python.org/moin/SandboxedPython列出了chroot监狱作为一种可能性,我认为这是一个有效的答案。 - Philip Tinney

1

嗯。这是一个思想实验,我不知道是否已经完成:

您可以使用编译器包来解析脚本。然后,您可以遍历此树,在所有标识符(变量、方法名称等)前缀中加上唯一的前导语,以便它们不可能引用您的变量。您还可以确保未调用编译器包本身,以及其他黑名单项,例如打开文件等。然后,您可以为此发出Python代码,并编译器.compile它。

文档指出,编译器包不在Python 3.0中,但没有提到3.0的替代方案是什么。

一般来说,这类似于论坛软件等尝试将“安全”的Javascript或HTML等列入白名单。历史上,它们在处理所有转义字符方面的记录都很糟糕。但是你可能会在Python中更加幸运 :)

4
请不要这样做。有很多种方法可以执行任意代码,而不直接使用您想要检查的软件包。例如,您可以遍历 ().__class__.__base__.__subclasses__() 的条目并搜索“code”条目,然后可以使用它来运行字符串中的代码。如果您将普通 Python 代码检查是否存在恶意内容,您永远无法确定自己是否忘记了检查可被利用的内容。 - Lukas Boersma

0

我认为目前为止,你最好的选择将是结合到目前为止收到的回复。

你需要解析和清洗输入 - 例如删除任何导入语句。

然后,你可以使用 Messa 的 exec 示例(或类似示例)来允许代码执行,只针对你选择的内置变量 - 最有可能是由你自己定义的某种 API ,以提供程序员访问你认为相关的功能。


我完全同意。这似乎是正确的方法。但我对你能够完成多少持怀疑态度。 - Noufal Ibrahim
哦,我需要使用Messa的方法来清理输入数据的哪些情况呢?我尝试导入模块或访问外部值,但似乎并不容易。由于没有可用的内置函数(import语句调用__import__函数),所以导入语句等已经被禁用了。 - Blixt
你应该试着找出在Python-dev上讨论这个问题的帖子。那里有很多方法可以打破沙盒。我找不到它了。 - Noufal Ibrahim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接