Sandbox(沙箱)到底是什么?从原理到代码实战
1. 什么是 Sandbox(沙箱)
Sandbox(沙箱)是一种安全机制,它把程序、进程或用户操作限制在一个受限的隔离环境中运行。沙箱里的代码可以使用一组受控的资源,比如读写指定的文件、调用指定的 API、占用有限的内存和 CPU;一旦它试图越界访问,就会被拦截、报错或直接终止。
可以把沙箱理解成给代码安排了一个「儿童游乐区」:孩子可以在围栏内部自由活动,但不能翻越围栏跑到马路上。围栏限制了活动范围,也保护了外面的世界。对软件来说,这个「围栏」就是权限控制、资源限制和访问隔离。
沙箱的核心目标有两个:
- 隔离:让不可信代码无法直接触碰宿主机、宿主进程或敏感数据。
- 限制:控制代码能访问什么样的资源,包括网络、文件、系统调用、内存、CPU 等。
一句话总结:沙箱不是某一个具体技术,而是一类「把不可信代码关进受限空间运行」的安全思想。实现它的手段可以很轻,比如浏览器里的 iframe 属性,也可以很重,比如虚拟机、容器和硬件虚拟化。
2. 为什么需要沙箱
在现代软件开发中,我们经常要运行「不完全可信」的代码,比如:
- 浏览器加载的第三方 JavaScript、广告脚本、页面内嵌内容。
- 在线代码编辑器、面试系统、OJ 评测平台里用户提交的代码。
- AI Agent、低代码平台里生成的脚本。
- 插件系统、第三方 SDK、自动化脚本。
这些代码一旦直接运行在宿主进程中,就可能读取本地文件、删除数据、访问内网、挖矿或窃取敏感信息。沙箱的价值就在于:即使代码是恶意的,它也无法对你造成超出沙箱边界的破坏。
下面是三种典型的威胁,以及沙箱如何应对:
| 威胁类型 | 没有沙箱时 | 有沙箱后 |
|---|---|---|
| 读取敏感文件 | 代码可直接读取宿主机文件系统 | 只能访问挂载进沙箱的虚拟文件系统 |
| 无限占用资源 | 死循环能拖垮整个进程 | CPU、内存、超时时间可被限制 |
| 访问内网服务 | 可探测和攻击内网端口 | 网络请求默认被禁止或只能白名单访问 |
3. 沙箱的常见实现层次
根据隔离强度和资源开销,沙箱可以分成多个层次。底层越深,通常越安全,但成本也越高。
- 浏览器级:使用 iframe 的
sandbox属性、Web Worker、同源策略、CSP 内容安全策略等,限制网页脚本的权限。 - 语言 / 虚拟机级:在语言运行时内创建受限执行上下文,例如 JavaScript 的
vm模块、Python 的restrictedpython或 Lua 的load限制环境,以及 JVM、WASM 的沙箱能力。 - 操作系统级:利用 seccomp、Landlock、AppArmor、SELinux、Linux Namespace 和 cgroup 等技术限制进程的系统调用与资源。
- 容器级:Docker、Podman、gVisor 等,通过 Namespace 和 cgroup 提供进程级隔离,比普通进程更安全,但共享宿主机内核。
- 虚拟化级:KVM、QEMU、Firecracker 等,运行独立内核,隔离最强,资源开销也最高。
下面通过三个最有代表性的实战案例,带你从「语言级」到「容器级」再到「浏览器级」理解沙箱是怎么工作的。
4. 代码实战一:Node.js vm 模块实现受限代码执行
Node.js 内置的 vm 模块可以创建一个独立的 JavaScript 执行上下文。要注意:vm 不是真正的安全沙箱,它适合运行「你信任但想隔离的代码」,不适合运行完全不可信的代码。对于高强度对抗场景,应搭配进程级隔离、容器或专用沙箱服务。
先看一个最基础的例子,展示如何在一个全新上下文中执行代码:
const vm = require("node:vm");
// 在独立上下文中执行代码
const script = new vm.Script("const x = 1; x + 1;");
const context = vm.createContext({});
const result = script.runInContext(context);
console.log(result); // 2
console.log(typeof x); // undefined,说明变量没有泄漏到外层作用域
上面的代码创建了一个全新的上下文,x 只存在于这个上下文内部,不会污染外部作用域。接下来我们给沙箱注入一些「受控能力」,同时把危险对象拒之门外:
const vm = require("node:vm");
function runUserCode(userCode, timeoutMs = 200) {
const sandbox = {
// 只暴露一个安全的日志函数
log: (message) => console.log("[sandbox]", message),
Math, // 只注入纯计算模块,不注入 fs、process、require
};
// 使用普通对象作为上下文
vm.createContext(sandbox);
const script = new vm.Script(userCode);
// 限制执行时间,防止死循环一直卡住进程
return script.runInContext(sandbox, {
timeout: timeoutMs,
});
}
try {
runUserCode( log("hello from sandbox"); log("2 的 10 次方 = " + Math.pow(2, 10)); );
} catch (err) {
console.error("执行失败:", err.message);
}
运行结果类似:
[sandbox] hello from sandbox
[sandbox] 2 的 10 次方 = 1024
因为沙箱对象里根本没有 require、process、fs 等对象,所以用户代码无法直接读写文件系统。再配合 timeout,就能拦截死循环:
try {
runUserCode("while(true) {}");
} catch (err) {
console.error("执行失败:", err.message); // 输出:Script execution timed out after 200ms
}
不过要特别强调:vm 模块只能做到「上下文隔离」,并不是字面意义上的「信任边界」。在某些老版本 Node 或特定写法下,恶意代码仍可能通过原型链等方式逃逸。所以真正面向不可信用户代码的场景,建议把 vm 放进容器或子进程里再运行,或者改用 isolated-vm 这类更完善的方案。
下面是使用子进程 + 资源限制的更强隔离思路:
const { spawn } = require("node:child_process");
function runInChild(code, { timeoutMs = 1000, maxMemory = 64 } = {}) {
return new Promise((resolve, reject) => {
const child = spawn("node", ["-e", code], {
timeout: timeoutMs,
stdio: ["ignore", "pipe", "pipe"],
});
let output = "";
let error = "";
child.stdout.on("data", (chunk) => (output += chunk));
child.stderr.on("data", (chunk) => (error += chunk));
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve(output);
} else {
reject(new Error(error || `子进程退出码:${code}`));
}
});
});
}
runInChild("console.log(1 + 1);")
.then((out) => console.log("输出:", out))
.catch((err) => console.error("失败:", err.message));
把用户代码丢给一个独立的 Node 子进程执行,即使子进程崩溃,主进程仍然存活。这已经接近「进程级沙箱」的思路了,下一步就可以用容器继续加固。
5. 代码实战二:Docker 容器实现进程级沙箱
容器是当前最常用的应用隔离方案。它基于 Linux Namespace 实现视图隔离,基于 cgroup 实现资源限制。相比 vm 模块,容器的隔离边界在操作系统层面,适合运行编译型程序、任意语言脚本和各种服务。
下面是一个「运行用户代码」的容器方案。先创建 Dockerfile:
# 使用体积较小的基础镜像
FROM node:20-slim
创建沙箱工作目录
WORKDIR /sandbox
创建非 root 用户,避免以 root 权限运行代码
RUN useradd -m runner
创建只读代码目录,并设置归属
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
以非特权用户执行
USER runner
ENTRYPOINT ["/entrypoint.sh"]
入口脚本 entrypoint.sh 负责把传入的代码写进沙箱目录后执行:
#!/bin/sh
set -e
将环境变量中的代码保存为文件
echo "$USER_CODE" > /sandbox/user-code.mjs
进入用户目录执行,禁止访问容器内的系统目录
cd /sandbox
node --max-old-space-size=64 user-code.mjs
构建并运行容器,同时做资源与网络限制:
# 构建镜像
docker build -t code-sandbox .
运行容器:
--network=none 禁止网络访问
--memory=128m 限制内存
--cpus=1 限制 CPU
--read-only 根文件系统只读
--tmpfs /sandbox 代码目录挂载为临时文件系统
docker run --rm
--network=none
--memory=128m
--cpus=1
--read-only
--tmpfs /sandbox:rw,size=16m
-e USER_CODE='console.log("safe code in docker")'
code-sandbox
这套参数组合的效果是:
--network=none:容器没有网络接口,代码无法对外发起请求,也无法扫描内网。--memory=128m和--cpus=1:限制资源使用,防止拖垮宿主机。--read-only和--tmpfs:容器根文件系统只读,只有沙箱目录可写,且每次运行结束即销毁。- 非 root 用户运行:即使代码想办法拿到容器内权限,也拿不到 root 权限。
容器默认共享宿主机内核,因此对于特别危险的代码,还需要考虑 gVisor、Kata Containers 或 Firecracker 这类更强的隔离方案,它们会让容器运行在独立的内核或轻量虚拟机中。
6. 代码实战三:浏览器 iframe sandbox 实现前端隔离
浏览器的 iframe 沙箱是最常见、也最容易上手的前端沙箱。只需要给 iframe 加上 sandbox 属性,浏览器就会对嵌入内容应用一组最小权限规则:禁止脚本执行、禁止表单提交、禁止弹窗、禁止顶层导航等。然后按需通过关键字重新开放某些权限。
下面是一个宿主页面,嵌入了被沙箱限制的第三方内容:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>iframe sandbox 示例</title>
</head>
<body>
<h2>沙箱中的内容</h2>
<!-- 只开放脚本执行权限,其余权限默认禁止 -->
<iframe
sandbox="allow-scripts"
src="sandbox-content.html"
width="600"
height="200"
></iframe>
</body>
</html>
被嵌入的 sandbox-content.html 尝试执行脚本、弹窗和读取父窗口:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
</head>
<body>
<h3>我是被沙箱限制的内容</h3>
<p id="result">正在执行脚本...</p>
<script>
// 允许执行脚本(因为开放了 allow-scripts)
document.getElementById("result").textContent = "脚本执行成功";
// 没有 allow-same-origin,无法读取父窗口
try {
console.log(window.parent.location.href);
} catch (err) {
console.warn("访问父窗口被禁止:", err.message);
}
// 没有 allow-modals,弹窗会被静默阻止
alert("这个弹窗不会出现");
</script>
</body>
</html>
常用的 sandbox 关键字如下:
| 关键字 | 作用 |
|---|---|
allow-scripts | 允许执行 JavaScript |
allow-same-origin | 将内容视为同源,可访问 Cookie 等;不能与 allow-scripts 在不可信内容上同时使用 |
allow-forms | 允许提交表单 |
allow-popups | 允许打开弹窗 |
allow-modals | 允许显示 alert、confirm、prompt 等模态框 |
allow-top-navigation | 允许内容导航顶层窗口 |
allow-downloads | 允许下载文件 |
前端沙箱不只是 iframe。Web Worker 把代码放在后台线程,无法访问 DOM;CSP 内容安全策略可以限制页面能加载的脚本和资源来源。三者经常组合使用,形成浏览器内的多层防护。
7. 沙箱不是万能的:局限与逃逸风险
沙箱极大降低了风险,但它不是绝对安全的。历史上发生过多次沙箱逃逸漏洞,攻击者通过浏览器、虚拟机或语言运行时的缺陷突破隔离边界。因此需要认清几条重要原则:
- 按信任等级选择方案:信任的插件可以用 vm;用户代码用子进程或容器;完全不可信且高价值目标用虚拟机甚至物理隔离。
- 纵深防御:不要只依赖一层沙箱,结合权限最小化、网络隔离、资源限制、日志审计和定期更新。
- 及时修复漏洞:沙箱本身也是软件,底层依赖存在漏洞时必须及时升级。
- 注意逃逸面:文件系统挂载、网络访问、strace、特殊设备、内核接口都可能是逃逸入口,默认应全部关闭,再按需开放。
对于需要运行高不可信代码的平台,推荐使用专用沙箱基础设施,比如 gVisor、Firecracker、WebAssembly 沙箱,或者云厂商提供的函数计算隔离环境。
8. 总结
Sandbox 的核心思想是「隔离 + 限制 + 最小权限」。它不是一个单点技术,而是一条覆盖浏览器、语言运行时、操作系统、容器和虚拟化的安全防线。
- 浏览器里,用 iframe sandbox、Web Worker 和 CSP 隔离第三方内容。
- Node.js 里,用 vm 模块做上下文隔离,必要时升级为子进程执行。
- 服务端平台里,用 Docker 等容器做进程级隔离,并配合网络限制、资源限制和非 root 用户。
- 高安全场景下,使用 gVisor、Firecracker 或虚拟机级别的隔离。
理解沙箱之后,再去设计在线代码运行平台、插件系统或 AI 工具时,就能更清楚地判断:这份代码该放进哪一层沙箱里,它能够做什么,以及它绝对不能碰到什么。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)