使用MicroPython和WASM在沙箱中安全运行Python代码
本文介绍了一种利用MicroPython和WebAssembly在Python应用中安全执行沙箱代码的新方法,解决了插件系统面临的安全风险,并提供了内存、CPU、文件及网络访问的严格限制。
使用MicroPython和WASM在沙箱中运行Python代码2026年6月6日几年来,我一直在尝试不同的方法在沙箱中运行代码,但我的最新尝试感觉它终于可能具备了我一直在寻找的所有特性。
我将其作为名为micropython-wasm的Alpha包发布,并且正在用它为Datasette Agent构建一个代码执行沙箱插件,名为datasette-agent-micropython。
为什么我需要沙箱?我的关键开源项目——Datasette、LLM,甚至sqlite-utils——都支持插件。我绝对喜欢插件作为扩展软件的机制。一个精心设计的插件系统可以将尝试新事物的风险降到几乎为零——即使是最疯狂的想法也不会对核心应用本身产生持久影响。
我的软件可以在一夜之间增加新功能,而我甚至不需要审查一个拉取请求!但有一个主要缺点:我的插件系统都使用Python和Pluggy,插件代码在我的应用中拥有完全权限执行。一个有缺陷或恶意的插件可能破坏一切或泄露私人数据。
我希望能够在一个无法读取未授权文件、无法连接网络、或无法以对应用其他部分或用户计算机有风险或有害的方式运行插件式代码。我的兴趣不仅仅局限于插件。
特别是对于Datasette,我想支持许多功能,其中任意代码执行会很有用。我已经在Datasette Enrichments中尝试过这一点,其中代码可用于转换表中存储的值。
我很想构建一种机制,让你可以按计划运行代码,从授权位置获取JSON,运行一小段代码将其重新格式化为字典列表,然后将其作为行插入到SQLite数据库表中。我对沙箱的需求我的目标是在我自己的Python应用中安全地执行代码。
以下是我需要的:- 依赖项能从PyPI干净安装,必要时包括跨多个平台的二进制wheel。我不希望使用我软件的人除了直接安装我的Python包外,还需采取任何额外步骤。- 执行的代码必须同时受内存和CPU限制。
我不希望while True: s += "longer string"导致我的应用或用户计算机崩溃。- 文件访问必须严格控制。要么完全没有文件系统访问,要么我能精确定义哪些文件可读、哪些可写。
- 网络访问也受控制。沙箱代码未经我完全控制的层,不能与任何内容通信。- 支持与宿主函数交互。如果不能仔细地将选定的平台功能暴露给运行的代码,沙箱就没多大用处。
- 它必须稳健、有支持且文档清晰。我已经记不清在仓库中看到过多少沙箱项目,并附有警告说它们未被积极维护!WebAssembly在这里看起来很有前景当涉及恶意代码时,Web浏览器在可想象的最恶劣环境中运行。
它们的工作是在几乎每次页面加载时从网络下载并执行不受信任的代码。鉴于此,JavaScript引擎应该是沙箱的优秀候选。遗憾的是,这些引擎也极其复杂,且不易嵌入其他项目。我见过的大多数v8-in-Python项目维护不频繁,并带有警告不要将其用于完全不受信任的代码。
WebAssembly是更好的候选。它从一开始就设计为支持我关心的所有特性,并已在浏览器中测试了近十年。wasmtime Python库维护活跃,并带有二进制wheel。
WebAssembly中的MicroPythonwasmtime等WebAssembly引擎运行WebAssembly二进制文件。Rust等一些编程语言很容易直接编译为WebAssembly。
JavaScript和Python等动态语言则更难——它们支持eval()等语言原语,这意味着它们需要在运行时拥有完整的解释器。要运行Python,我们需要一个完整的Python解释器编译为WebAssembly,并以一种易于提供代码、连接宿主函数和访问结果的方式连接。
Pyodide提供了一个出色的包,用于在浏览器中使用WebAssembly运行Python,但不支持在服务器端Python中使用Pyodide。
我能找到的最新建议来自2024年10月,指出“Pyodide由Emscripten工具链构建,只能在浏览器或Node.js中运行”。前几天,我决定考虑MicroPython作为这个选项。
MicroPython网站说:MicroPython是Python 3编程语言的一个精简而高效的实现,包含Python标准库的一小部分,并针对微控制器和受限环境进行了优化。WebAssembly对我来说确实感觉像一个受限环境!
构建第一个版本我让GPT-5.5 Pro为我做了一些研究,发现了Yamamoto Takahashi针对MicroPython的一个PR,标题为“Experimental WASI support for ports/unix”。
然后它生成了这个research.md文档,所以我让Codex Desktop和GPT-5.5 high loose来处理,看看会发生什么:阅读research.md文档并构建这个。
作为该项目的一部分,你可能需要编写一个脚本,编译一个自定义的WASM版本的MicroPython——作为该脚本的一部分,将MicroPython代码获取到/tmp目录。它成功了。我现在有了一个原型Python库,可以在WebAssembly沙箱内执行Python代码!
最棘手的部分是持久解释器状态。
我们这里使用的WASM构建暴露了一个单一入口点,它启动解释器,运行代码,然后在最后停止解释器。这对于一次性脚本来说很好,但对于Datasette Agent,我希望变量和函数驻留在内存中,以便在多次代码执行调用中重用它们。
使用编码代理的一个好处是,你可以快速从想法到概念验证。
我提示:为了保持变量驻留:如果我们在MicroPython内部运行代码,调用一个宿主函数get_next_python_code(),然后将其传递给eval()——并且该宿主函数阻塞直到新代码可用,也许通过在一个带有队列的线程中运行?
这个或类似的想法能帮上忙吗?经过一些迭代,我们得到了一个可行的版本!
在Python代码中,你现在可以这样做:from micropython_wasm import MicroPythonSessionwith MicroPythonSession() as session:
print(session.run("x = 10\nprint(x)").stdout) print(session.run("x += 5\nprint(x)").stdout) print(session.run("print(x * 2)").stdout)在底层
,它启动一个线程,建立一个请求队列,然后向该队列发送消息给session.run()命令,每次等待回复队列以获取该执行的结果。
在WASM内部,MicroPython解释器阻塞等待__session_next__()宿主函数返回下一行代码,它运行eval(),然后在每个块成功执行时调用__session_result__({"id": request_id, "ok": True})。
另一个复杂部分是支持宿主函数,这样我的Python库可以有选择地暴露函数,供MicroPython中运行的代码调用。Codex最终用78行C代码解决了这个问题,这些C代码被编译成我随包分发的362KB WebAssembly blob。
我绝不是C程序员,但我读过这些C代码,并让两个不同的模型向我解释(这是Claude的解释),并且我对它进行了一系列测试。使用WebAssembly的好处是,如果C代码存在致命缺陷,最坏的情况是WebAssembly执行失败并抛出异常。
我可以接受这种风险。