AI 编码代理通常会在执行命令前请求确认,但“需要批准”不等于“命令可信”。当代理进入一个受攻击者影响的仓库时,你批准的可能只是熟悉的 npm test、make test 或 pytest;真正运行的代码,却藏在 package.json、Makefile、测试夹具、编译插件或依赖安装脚本中。
问题的关键不只是代理会执行什么命令,而是这条命令能接触到什么。Docker Sandbox 一类隔离环境的价值,正是把攻击发生后的可达范围压缩到容器内部。
危险藏在命令背后
假设代理建议执行:
npm test
这条命令看起来很普通,但 npm 会根据仓库中的 package.json 解析脚本。一个经过篡改的项目可以这样定义它:
{
"name": "demo-project",
"private": true,
"scripts": {
"pretest": "node scripts/prepare-test.js",
"test": "node --test"
}
}
用户批准的是 npm test,实际执行链却包括仓库作者控制的 scripts/prepare-test.js。同样的问题也可能出现在以下入口:
make test会解释仓库中的Makefile。pytest会导入测试模块、插件和conftest.py。pip install .、npm install等操作可能触发构建或生命周期脚本。- 编译器、代码生成器和格式化工具可能加载项目级插件或配置。
因此,审批界面只能说明“用户同意运行这个入口命令”,不能证明命令展开后的全部行为安全。只看命令名称,会遗漏仓库内容、工具约定和依赖解析共同构成的执行链。
攻击半径取决于代理能看到什么
如果编码代理直接在开发者主机上运行,恶意代码可能接触当前用户拥有的资源,例如:
~/.ssh、云平台凭据和 Git 凭据;- 浏览器会话、环境变量和本地配置文件;
- 已挂载的公司代码库与共享目录;
- 本机网络可访问的数据库、元数据服务和内部 API;
- Docker Socket,以及它背后的宿主机控制能力。
容器不能让恶意代码变得可信,但可以改变后果。一个边界清晰的 Sandbox 应只挂载当前任务所需的工作目录,使用非 root 用户,删除 Linux capabilities,限制网络,并避免向容器传入宿主机密钥。
这里必须强调边界:把代码放进容器并不自动等于安全。以读写方式挂载整个主目录、传入云凭据,或者挂载 /var/run/docker.sock,都会让隔离效果大幅下降。容器还共享宿主机内核;面对高风险、不受信任的代码时,虚拟机或更强的沙箱运行时通常能提供更明确的隔离边界。
可以这样实践:建立一个最小权限工作区
下面是一个可直接改造的 Docker Compose 示例。它把当前仓库挂载到 /workspace,根文件系统设为只读,使用非 root 用户,并默认关闭网络。
将以下内容保存为 compose.sandbox.yaml:
services:
agent-shell:
image: node:22-bookworm-slim
working_dir: /workspace
user: "1000:1000"
command: ["sleep", "infinity"]
network_mode: "none"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
pids_limit: 256
mem_limit: 2g
cpus: 2
tmpfs:
- /tmp:rw,noexec,nosuid,size=256m
- /home/node:rw,nosuid,size=128m
volumes:
- .:/workspace:rw
运行前需要确认宿主机用户的 UID/GID。示例中的 1000:1000 适合不少 Linux 工作站,但不是通用值,可通过以下命令查看:
id -u
id -g
然后启动沙箱并在其中执行测试:
docker compose -f compose.sandbox.yaml up -d
docker compose -f compose.sandbox.yaml exec agent-shell npm test
docker compose -f compose.sandbox.yaml down
这个配置的意图很具体:即使 npm test 间接执行了仓库中的攻击代码,它默认也无法访问宿主机主目录,不能直接联网,不能写入容器系统目录,也没有额外 capabilities。
它仍然能够修改当前仓库,因为工作区是读写挂载的。对于只需要代码审查、静态分析或生成补丁预览的任务,可以进一步改为只读:
volumes:
- .:/workspace:ro
如果工具必须写入构建目录,可以单独提供一个临时卷,而不是开放整个仓库:
volumes:
- .:/workspace:ro
- build-output:/workspace/dist
volumes:
build-output:
实际项目还需要根据技术栈调整镜像与命令。例如 Python 项目可把镜像换成 python:3.13-slim,再执行 python -m pytest。这些配置是可以采用的隔离实践,并不意味着某个特定 Sandbox 产品只支持这一种实现。
网络不能只分成“开”和“关”
完全禁网适合本地测试、静态分析和已有依赖缓存的任务,但首次安装依赖通常需要访问包仓库。直接恢复全部网络访问虽然方便,也让恶意脚本获得外传数据和探测内网的通道。
更稳妥的工作流是把依赖获取与不受信任代码执行拆开:
- 在受控阶段生成并审核锁文件。
- 通过内部镜像或代理下载依赖并建立缓存。
- 在禁网沙箱中安装缓存内容并运行测试。
- 只有明确需要访问外部服务的任务,才临时开放目标域名或代理。
还应避免把秘密作为普通环境变量注入整个代理会话。环境变量对进程及其子进程通常可见,仓库中的测试代码也可能读取它们。确需访问凭据时,应使用短期、低权限、限定目标资源的令牌,并让凭据只在最短的命令阶段出现。
落地时检查这些边界
为编码代理设计执行环境时,可以逐项确认:
- 默认把仓库视为不受信任输入,即使入口命令看起来熟悉。
- 只挂载当前任务需要的目录,不挂载整个主目录。
- 不把 SSH、Git、云平台凭据永久注入沙箱。
- 不挂载 Docker Socket;它往往等价于把宿主机控制权交给容器。
- 使用非 root 用户,并启用
no-new-privileges、capability 删除和资源上限。 - 默认关闭网络,再按任务开放最小访问范围。
- 将依赖下载、构建、测试和发布拆成权限不同的阶段。
- 对工作区变更保留 diff,任务结束后销毁临时容器与卷。
- 对高价值代码、生产凭据或强对抗场景,考虑虚拟机或专用隔离运行时。
命令审批仍然有价值,但它解决的是交互授权,不是代码可信度。真正可靠的策略,是假设一次合理的批准最终可能触发攻击者控制的代码,然后通过文件系统、网络、身份与资源边界,把这次执行能够造成的影响限制在可接受范围内。