代码执行与沙箱:让AI安全地“写代码、跑代码” ——CogitoAgent开发实战(七)
代码执行与沙箱:让AI安全地“写代码、跑代码”
——CogitoAgent开发实战(第7篇)
📖 本文是专栏的第七篇。前六篇我们让AI学会了思考、动手、管理文件、联网、说话、记忆。但有一个能力始终没解锁——写代码并执行。让AI写代码不难,难的是让它安全地执行代码——万一它写了个
rm -rf /怎么办?这一篇,我们深入代码执行与沙箱系统,看看CogitoAgent如何在“给AI能力”和“保护用户系统”之间取得平衡。

📌 从一个担忧开始
假设你要把一把刀交给一个机器人。
你会担心:它会不会乱砍东西?会不会伤到自己?会不会伤到我?
让AI执行代码,是同样的问题。
AI能写代码,但如果它能直接执行代码,风险有多大?
| 风险场景 | 可能后果 |
|---|---|
| AI写了一个删除命令 | 用户文件被删除 |
| AI写了一个网络请求 | 敏感数据被发送到外部 |
| AI写了一个死循环 | CPU被占满,系统卡死 |
| AI写了一个恶意脚本 | 系统被入侵 |
所以,代码执行功能必须用沙箱隔离。
一、两种语言的执行方案
CogitoAgent 支持两种语言的代码执行:
| 语言 | 执行方式 | 安全策略 |
|---|---|---|
| JavaScript | Node.js vm 模块 |
沙箱隔离,禁止文件/网络访问 |
| Python | 临时文件 + execFile |
环境隔离,限制资源访问 |
两种语言的安全策略不同,因为它们的执行环境不同。
二、JavaScript 执行:vm 沙箱
2.1 什么是 vm 模块?
Node.js 的 vm 模块提供了一个隔离的上下文环境,可以在其中执行 JavaScript 代码,而不会污染主进程。
import vm from 'vm';
const code = 'console.log("Hello")';
const context = vm.createContext({ console });
vm.runInContext(code, context);
2.2 沙箱的核心防御
CogitoAgent 的沙箱实现了几层防御:
第一层:无原型链的对象
function createSandbox() {
// Object.create(null) 创建没有原型的对象
const sandbox = Object.create(null);
// 只有明确添加的属性才存在
sandbox.console = { log, error, warn, info };
sandbox.JSON = Object.freeze(JSON);
sandbox.Math = Object.freeze(Math);
// ...
return { sandbox, context: vm.createContext(sandbox) };
}
为什么 Object.create(null) 能防逃逸?
大多数 JavaScript 对象都有原型链:{}.constructor 指向 Object,Object.constructor 指向 Function,通过原型链可以访问到 Function 构造函数,进而执行任意代码。
Object.create(null) 创建的对象没有原型,{}.constructor 是 undefined,原型链攻击路径被切断。
第二层:冻结内置对象
sandbox.JSON = Object.freeze(JSON);
sandbox.Math = Object.freeze(Math);
sandbox.Array = Object.freeze(Array);
// ...
Object.freeze() 防止对象被修改,避免通过篡改内置对象来逃逸。
第三层:移除危险对象
sandbox.setTimeout = undefined;
sandbox.setInterval = undefined;
sandbox.setImmediate = undefined;
sandbox.process = undefined;
sandbox.require = undefined;
sandbox.__dirname = undefined;
sandbox.__filename = undefined;
// ...
移除了所有可能访问外部资源的能力。
2.3 执行流程
async function runJavaScript(code) {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
resolve({ success: false, error: '执行超时(超过30秒)' });
}, MAX_EXECUTION_TIME);
try {
const { context } = createSandbox();
// 代码包裹在函数中,捕获返回值
const wrappedCode = `
(function() {
try {
return { success: true, result: (function() { ${code} })() };
} catch (e) {
return { success: false, error: e.message };
}
})()
`;
const result = vm.runInContext(wrappedCode, context, {
timeout: MAX_EXECUTION_TIME,
displayErrors: true
});
clearTimeout(timeoutId);
if (result.success) {
// 格式化输出
let output = result.result === undefined
? '执行完成,无返回值'
: typeof result.result === 'object'
? JSON.stringify(result.result, null, 2)
: String(result.result);
// 截断过长输出
if (output.length > MAX_OUTPUT_SIZE) {
output = output.slice(0, MAX_OUTPUT_SIZE) + '\n\n[输出内容过长,已截断]';
}
resolve({ success: true, data: output });
} else {
resolve({ success: false, error: `执行失败: ${result.error}` });
}
} catch (e) {
clearTimeout(timeoutId);
resolve({ success: false, error: `执行失败: ${e.message}` });
}
});
}
2.4 安全边界:能做什么,不能做什么
在沙箱中可以:
- 执行纯计算(数学运算、字符串处理)
- 使用
console.log输出(输出被捕获) - 使用
JSON、Math、Date等内置对象 - 使用
Array、Map、Set等数据结构
在沙箱中不能:
- 读取或写入文件(没有
fs) - 发起网络请求(没有
fetch、http) - 访问
process环境变量 - 使用
require或import - 执行
setTimeout或setInterval - 访问主进程的任何变量
2.5 非沙箱模式(危险!)
CogitoAgent 提供了一个非沙箱模式,通过环境变量 COGITO_SANDBOX_MODE=false 启用。
async function runJavaScriptDirect(code) {
console.warn('[安全警告] 非沙箱模式执行 JavaScript 代码,可能存在安全风险');
// 使用 Function 构造器执行
const fn = new Function(code);
const result = fn();
// ...
}
这个模式很危险,因为代码可以访问 process、require、文件系统等。只在完全信任代码来源时使用。
三、Python 执行:临时文件隔离
3.1 为什么 Python 不用 vm?
vm 是 Node.js 的模块,只能执行 JavaScript。Python 代码需要 Python 解释器执行,所以方案不同。
3.2 核心安全策略
CogitoAgent 通过以下方式隔离 Python 执行:
- 临时文件:代码写入临时文件,不直接传递
- execFile 而非 exec:参数通过数组传递,防止命令注入
- 环境变量限制:删除危险环境变量
- 工作目录限制:在
os.tmpdir()中执行 - 超时保护:30秒超时
3.3 安全执行流程
async function runPython(code) {
return new Promise(async (resolve) => {
let tmpPath = null;
try {
// 1. 生成安全的临时文件路径
tmpPath = generateSecureTmpPath('py');
// 2. 写入临时文件(exclusive 模式,防覆盖)
await writeSecureTmpFile(tmpPath, code);
// 3. 安全环境配置
const secureEnv = {
...process.env,
PYTHONUNBUFFERED: '1', // 无缓冲输出
PYTHONNOUSERSITE: '1', // 禁止用户站点包
PYTHONHASHSEED: '0', // 固定哈希种子
PYTHONDONTWRITEBYTECODE: '1', // 不生成 .pyc
// 限制 PATH
PATH: process.env.PATH?.split(path.delimiter).slice(0, 3).join(path.delimiter) || '',
};
// 删除危险环境变量
delete secureEnv.PYTHONPATH;
delete secureEnv.PYTHONHOME;
delete secureEnv.VIRTUAL_ENV;
// 4. 使用 execFile 执行
execFile('python', [tmpPath], {
timeout: MAX_EXECUTION_TIME,
encoding: 'utf8',
cwd: os.tmpdir(), // 在临时目录执行
env: secureEnv, // 使用安全环境变量
maxBuffer: MAX_OUTPUT_SIZE * 2,
}, async (error, stdout, stderr) => {
// 5. 清理临时文件
await cleanupTmpFile(tmpPath);
if (error) {
resolve({ success: false, error: `${error.message}\n${stderr || ''}` });
return;
}
resolve({ success: true, data: stdout || '执行完成,无输出' });
});
} catch (e) {
await cleanupTmpFile(tmpPath);
resolve({ success: false, error: e.message });
}
});
}
3.4 临时文件的安全细节
生成唯一文件名:
function generateSecureTmpPath(ext) {
const tmpDir = os.tmpdir();
const randomName = `cogito_${crypto.randomUUID()}_${Date.now()}`;
return path.join(tmpDir, `${randomName}.${ext}`);
}
crypto.randomUUID() 生成全局唯一标识符,防止文件名冲突和预测。
独占写入:
async function writeSecureTmpFile(filePath, content) {
const fd = await fs.open(filePath, 'wx'); // 'w' + 'x' = 独占创建
// 如果文件已存在,'wx' 会报错 EEXIST
await fd.writeFile(content, 'utf8');
await fd.close();
}
'wx' 标志确保文件是新建的,如果已存在则失败,防止覆盖和符号链接攻击。
超时清理:
const timeoutId = setTimeout(async () => {
timedOut = true;
await cleanupTmpFile(tmpPath); // 超时时也清理临时文件
resolve({ success: false, error: '执行超时(超过30秒)' });
}, MAX_EXECUTION_TIME);
即使超时,临时文件也会被清理,不留残留。
3.5 GUI 程序的特殊处理
Python 程序可能打开 GUI 窗口(如 tkinter、matplotlib)。这些程序会阻塞执行,因为 mainloop() 不会退出。
CogitoAgent 检测 GUI 代码并注入超时退出机制:
function hasGuiBlockingCall(code) {
const guiPatterns = [
/\.mainloop\s*\(/, // tkinter
/\.show\s*\(/, // PyQt
/app\.exec\s*\(/, // PyQt
/plt\.show\s*\(/, // matplotlib
/turtle\.mainloop/, // turtle
];
return guiPatterns.some(pattern => pattern.test(code));
}
function injectGuiTimeout(code) {
const guiWrapper = `
import threading
import time
import sys
def _gui_timeout_exit(delay=10):
time.sleep(delay)
print("\\n[提示] GUI 窗口将在 10 秒后自动关闭...")
sys.exit(0)
_thread = threading.Thread(target=_gui_timeout_exit, daemon=True)
_thread.start()
${code}
`;
return guiWrapper;
}
这样,GUI 程序会在 10 秒后自动退出,不会永久阻塞。
四、两种语言的统一接口
4.1 executeCode:自动识别语言
async function executeCode(code, language = 'javascript') {
const lang = language.toLowerCase();
if (lang === 'python' || lang === 'py') {
return await runPython(code);
} else if (lang === 'javascript' || lang === 'js') {
return await runJavaScript(code);
} else {
return { success: false, error: `不支持的语言: ${language}` };
}
}
4.2 executeFile:执行代码文件
async function executeFile(filePath, language = null) {
const content = await fs.readFile(filePath, 'utf-8');
if (!language) {
const ext = path.extname(filePath).toLowerCase();
if (ext === '.py') language = 'python';
else if (ext === '.js') language = 'javascript';
else {
return { success: false, error: '无法自动识别文件语言,请指定 language 参数' };
}
}
return await executeCode(content, language);
}
4.3 统一的安全参数
| 参数 | 默认值 | 说明 |
|---|---|---|
MAX_EXECUTION_TIME |
30000ms | 超时限制 |
MAX_OUTPUT_SIZE |
100000字符 | 输出大小限制 |
五、沙箱的局限性
5.1 vm 沙箱的已知逃逸路径
原生 vm 模块不是完全安全的。已知的逃逸路径包括:
通过 Error.prepareStackTrace:
// 在沙箱中执行
const err = new Error();
Error.prepareStackTrace = (err, stack) => {
// 可以访问外部上下文
return process.cwd(); // 如果 process 被暴露
};
通过代理对象:
// 如果沙箱中有代理对象,可能通过代理访问外部
new Proxy({}, {
get: (target, prop) => {
// 可以访问外部变量
}
});
5.2 Python 临时文件的局限
Python 代码通过临时文件执行,但 Python 进程本身不在沙箱中。这意味着:
- Python 代码可以访问文件系统(通过
open()) - Python 代码可以发起网络请求(通过
requests) - Python 代码可以执行系统命令(通过
os.system())
虽然我们用环境变量限制了部分能力,但这不是完全隔离。
5.3 权衡:能力 vs 安全
| 方案 | 安全性 | 能力 | 适用场景 |
|---|---|---|---|
| 完全沙箱(vm2/容器) | 高 | 受限 | 运行不可信代码 |
| 原生 vm | 中等 | 受限 | 运行半可信代码 |
| 临时文件 + 环境限制 | 中等 | 较自由 | 运行用户代码 |
| 直接执行 | 低 | 完全 | 运行可信代码 |
CogitoAgent 选择的是中等安全方案——足以防止意外破坏,但不足以防御恶意攻击。
六、与工具系统的集成
6.1 在工具注册表中
// registry.js
const TOOL_REGISTRY = {
// ...
executeCode: { fn: tools.executeCode, argCount: 2 },
executeFile: { fn: tools.executeFile, argCount: 2 },
runJavaScript: { fn: tools.runJavaScript, argCount: 1 },
runPython: { fn: tools.runPython, argCount: 1 },
formatCode: { fn: tools.formatCode, argCount: 2 },
// ...
};
6.2 在系统提示词中
// prompt.js
## 代码执行工具
- executeCode(code, language) - 执行代码,支持 JavaScript 和 Python
- executeFile(filePath, language) - 执行代码文件
- runJavaScript(code) - 执行 JavaScript 代码
- runPython(code) - 执行 Python 代码
- formatCode(code, language) - 格式化代码
6.3 AI 使用示例
用户:帮我写一个 Python 脚本,计算斐波那契数列前10项,然后运行它。
AI:好的,我来编写并执行。
[TOOL] executeCode("def fib(n): ...", "python") [/TOOL]
[工具结果]: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
七、安全设计回顾
| 安全措施 | 用途 | 实现位置 |
|---|---|---|
| 无原型链沙箱 | 防止原型链逃逸 | createSandbox() |
| 冻结内置对象 | 防止篡改内置对象 | Object.freeze() |
| 移除危险对象 | 防止访问外部资源 | sandbox.process = undefined |
| 超时保护 | 防止死循环 | setTimeout |
| 输出限制 | 防止上下文爆炸 | MAX_OUTPUT_SIZE |
| 临时文件隔离 | 防止命令注入 | writeSecureTmpFile |
| 环境变量限制 | 防止恶意配置 | secureEnv |
| 独占写入 | 防止文件覆盖 | 'wx' 标志 |
| 超时清理 | 防止文件残留 | cleanupTmpFile |
八、小结
这一篇讲了代码执行与沙箱系统:
| 语言 | 执行方式 | 安全策略 |
|---|---|---|
| JavaScript | vm.runInContext |
无原型链 + 冻结对象 + 移除危险API |
| Python | 临时文件 + execFile |
环境隔离 + 路径限制 + 超时保护 |
核心设计原则:
- 默认隔离,不给危险权限
- 超时保护,防止资源耗尽
- 输出限制,防止上下文爆炸
- 临时文件自动清理,不留残留
- 提供非沙箱模式(用户自担风险)
下一篇预告:Git 集成
我们将深入 git.js,看看:
- Git 命令如何安全执行(白名单 + 参数验证)
- 如何防止命令注入攻击
- 完整的 Git 操作封装
如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!
更多推荐




所有评论(0)