代码执行与沙箱:让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 指向 ObjectObject.constructor 指向 Function,通过原型链可以访问到 Function 构造函数,进而执行任意代码。

Object.create(null) 创建的对象没有原型{}.constructorundefined,原型链攻击路径被切断。

第二层:冻结内置对象

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 输出(输出被捕获)
  • 使用 JSONMathDate 等内置对象
  • 使用 ArrayMapSet 等数据结构

在沙箱中不能

  • 读取或写入文件(没有 fs
  • 发起网络请求(没有 fetchhttp
  • 访问 process 环境变量
  • 使用 requireimport
  • 执行 setTimeoutsetInterval
  • 访问主进程的任何变量

2.5 非沙箱模式(危险!)

CogitoAgent 提供了一个非沙箱模式,通过环境变量 COGITO_SANDBOX_MODE=false 启用。

async function runJavaScriptDirect(code) {
  console.warn('[安全警告] 非沙箱模式执行 JavaScript 代码,可能存在安全风险');
  // 使用 Function 构造器执行
  const fn = new Function(code);
  const result = fn();
  // ...
}

这个模式很危险,因为代码可以访问 processrequire、文件系统等。只在完全信任代码来源时使用。


三、Python 执行:临时文件隔离

3.1 为什么 Python 不用 vm?

vm 是 Node.js 的模块,只能执行 JavaScript。Python 代码需要 Python 解释器执行,所以方案不同。

3.2 核心安全策略

CogitoAgent 通过以下方式隔离 Python 执行:

  1. 临时文件:代码写入临时文件,不直接传递
  2. execFile 而非 exec:参数通过数组传递,防止命令注入
  3. 环境变量限制:删除危险环境变量
  4. 工作目录限制:在 os.tmpdir() 中执行
  5. 超时保护: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 环境隔离 + 路径限制 + 超时保护

核心设计原则

  1. 默认隔离,不给危险权限
  2. 超时保护,防止资源耗尽
  3. 输出限制,防止上下文爆炸
  4. 临时文件自动清理,不留残留
  5. 提供非沙箱模式(用户自担风险)

下一篇预告:Git 集成

我们将深入 git.js,看看:

  • Git 命令如何安全执行(白名单 + 参数验证)
  • 如何防止命令注入攻击
  • 完整的 Git 操作封装

如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!

👉 https://gitee.com/cnt-code/cogito-agent 👈

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐