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

因为沙箱对象里根本没有 requireprocessfs 等对象,所以用户代码无法直接读写文件系统。再配合 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 工具时,就能更清楚地判断:这份代码该放进哪一层沙箱里,它能够做什么,以及它绝对不能碰到什么。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐