如何在纯Python中实现沙箱环境?

84 投票
7 回答
61707 浏览
提问于 2025-04-16 00:08

我正在用纯Python开发一个网页游戏,想要一些简单的脚本功能,这样可以让游戏内容更加动态。特权用户可以实时添加游戏内容。

如果脚本语言能用Python就好了。不过,直接在游戏运行的环境中执行Python是不行的,因为恶意用户可能会造成很大的破坏,这样就不好了。那么,有没有办法在纯Python中运行一个受限制的Python环境呢?

更新:其实,真正的Python支持可能太复杂了,使用一种语法类似Python的简单脚本语言就非常合适。

如果没有类似Python的脚本解释器,那有没有其他用纯Python写的开源脚本解释器可以用呢?我需要的功能包括支持变量、基本的条件判断和函数调用(不是定义)。

7 个回答

12

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

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

但是在这样的环境里,你几乎什么都做不了 :)(连import模块都不行;不过,恶意用户还是可以让程序无限循环或者耗尽内存。)所以你可能想要添加一些模块,这些模块可以作为你游戏引擎的接口。

18

大约在原问题提出十年后,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()内部解除这个限制似乎是徒劳的,因为审计钩子使用的是一个不同的locals副本,而这个副本对exec运行的代码是不可访问的。请纠正我如果我错了。

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

当然,顶层代码可以再次启用文件输入输出。

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

63

这事儿其实挺复杂的。

有两种方法可以让Python运行在一个受限制的环境里。第一种是创建一个限制环境(也就是说,里面的全局变量很少等),然后在这个环境里用exec来执行你的代码。这就是Messa所建议的。这种方法不错,但有很多方法可以突破这个限制,造成麻烦。大约一年前,Python开发者社区里有个讨论,大家分享了从捕获异常到操作内部状态,再到字节码处理的各种方法,都是为了突破这个限制。如果你想要一个完整的编程语言,这种方法是不错的选择。

第二种方法是先解析代码,然后使用ast模块去掉你不想要的部分(比如导入语句、函数调用等),然后编译剩下的部分。如果你想把Python当作配置语言使用,这种方法更合适。

还有一种方法(可能不适合你,因为你在用GAE),就是PyPy沙箱。虽然我自己没用过,但网上说这是唯一一个真正的沙箱Python。

根据你描述的需求(支持变量、基本条件判断和函数调用(而不是定义)),你可能想考虑第二种方法,把其他不需要的部分从代码中去掉。这有点棘手,但还是可以做到的。

撰写回答