本文详解Agent动态Skill供应链安全的第二阶段:执行隔离沙箱实战。通过Python审计钩子、环境变量白名单、强超时与输出上限三项基础约束,以及进程级软沙箱和容器级硬沙箱(Docker)两种模式,彻底阻断恶意脚本对宿主环境的访问。文章提供完整的Java执行器与Python引导脚本源码及接入指南。
最终效果:安全隔离的执行环境
在执行隔离沙箱加固后,无论Skill脚本是否携带恶意代码,其执行过程都被严格限制在预设的边界内。以下是加固后的核心表现:
- 环境隔离:子进程无法读取宿主机的 `OPENAI_API_KEY`、数据库密码等敏感环境变量。
- 网络封锁:脚本尝试联网(如 `socket` 调用)会被审计钩子直接拦截并抛出 `PermissionError`。
- 资源受限:恶意脚本无法无限占用内存或CPU,超时后进程树会被强制销毁。
- 文件围栏:脚本只能读写指定的工作目录,无法越权修改宿主文件系统。
前置条件与准备说明
在接入沙箱执行器之前,请确保开发环境满足以下条件:
- JDK 9 以上:用于运行 Java 侧的沙箱执行器。
- Python 3.8 以上:用于执行动态 Skill 脚本。
- uv 工具(可选):如果 Skill 包含 Python 依赖,建议使用 `uv` 进行依赖解析。
- Docker(可选):若启用容器级硬沙箱,需确保宿主机已安装并配置好 Docker。
第一阶段:设计基础执行约束
沙箱的第一层防线是三项基础执行约束,它们与沙箱开关无关,永远生效。这三项约束保证了基本的执行纪律:密钥不传给子进程、跑不完就杀掉、输出太多就截断。
第1步:配置环境变量白名单
默认情况下,`ProcessBuilder` 会继承宿主进程的全部环境变量。实测表明,一个空脚本通过 `print(os.environ)` 就能获取宿主机上的 `OPENAI_API_KEY`、`ANTHROPIC_AUTH_TOKEN` 甚至数据库连接串。因此,必须在 Java 侧执行前清空并重建环境变量:
- 调用 `pb.environment().clear()` 清空继承的环境。
- 仅从宿主机拷贝白名单内的键(如 `PATH`, `SYSTEMROOT`, `LANG`)。
- 显式注入沙箱内部约定的变量(如 `SKILL_SANDBOX_WORKDIR`)。
通过这种方式,`DATABASE_URL`、`NACOS_PASSWORD` 等敏感信息从源头被切断,脚本即使带恶意也无法读取。
第2步:实施强超时与进程树销毁
为了防止脚本陷入死循环或长时间占用资源,必须设置强超时。在 Java 侧,使用 `waitFor(timeoutSeconds, TimeUnit.SECONDS)` 判断是否超时。如果超时,不仅要调用 `destroyForcibly()` 杀掉父进程,还必须遍历 `process.descendants()` 将整棵进程树强制销毁,防止孙进程变成孤儿继续运行。
第3步:限制输出上限并排空管道
当脚本输出大量数据时,内存可能被耗尽。实现策略是:在读取标准输出流时,超过 `maxBytes` 的部分直接丢弃,但必须继续读取直到 EOF。如果停止读取,子进程会阻塞在写满的管道上,导致进程永远不退出。因此,截断输出并排空管道是防止资源耗尽的关键。
第二阶段:实现进程级软沙箱
进程级软沙箱通过一个 Python 引导脚本 `sandbox_bootstrap.py` 实现。它不直接运行目标脚本,而是先经过引导脚本进行资源限制、审计钩子注入和文件系统围栏。
第4步:使用 Python -I 隔离模式
启动 Python 解释器时加上 `-I` 参数,这会忽略所有 `PYTHON*` 环境变量、禁用 user site-packages,并从解释器层面与宿主环境切割。注意,这不会跳过 venv 的标准 site-packages,因此 `uv run` 建好的虚拟环境依赖仍可正常导入。
第5步:注入审计钩子拦截危险操作
在引导脚本中注册 `sys.addaudithook(audit)`,拦截以下危险通道:
socket.*:拦截所有联网操作,包括 `getaddrinfo`。subprocess.*:禁止启动新进程。os.system和ctypes.dlopen:分别拦截系统调用和加载原生库。
当脚本尝试执行这些操作时,钩子会抛出 `PermissionError` 中断操作。对于 `exec`/`eval` 等间接能力,由于误伤面大,软沙箱暂不拦截,留给容器级硬沙箱处理。
第6步:建立文件系统围栏
审计钩子必须兼容不同平台/版本的 `open` 事件参数形态(Windows 上是模式字符串,其他平台是 int 标志位)。围栏策略如下:
- 工作目录(workdir):每次执行新建的临时目录,可读可写。
- 只读根(ro_roots):包括脚本目录、Skill 目录、解释器 `sys.path` 以及通过 `SKILL_SANDBOX_READ_ROOTS` 指定的目录。脚本只能读取,不能写入。
任何尝试读写非白名单目录的操作都会被拦截。
第7步:以 __main__ 语义加载目标脚本
引导脚本最后使用 `runpy.run_path(script, run_name="__main__")` 加载目标脚本。这保持了脚本 `if __name__ == "__main__"` 的语义,同时把 `sys.argv` 重写为 `[脚本路径, 原参数...]`,对脚本来说和直接执行完全一致。
第三阶段:部署容器级硬沙箱
进程级软沙箱与脚本跑在同一个进程里,理论上存在被恶意代码绕过的风险(例如通过 `ctypes` 之外的途径调原生接口)。因此,生产环境建议启用容器级硬沙箱(Docker 模式)。
第8步:配置 Docker 容器参数
在 Docker 模式下,使用以下参数运行容器:
--network none:无网络。--read-only:根文件系统只读。--cap-drop ALL:丢弃全部 Linux capabilities。--security-opt no-new-privileges:禁止提权。--pids-limit 64:限制进程数上限。--memory 512m --cpus 0.5:限制内存和 CPU 配额。
注意:容器级硬沙箱要求预构建镜像,把 Python 运行时和 Skill 依赖直接打进镜像。因为 `--read-only` 下 `uv run` 无法创建 `.venv`。
第四阶段:接入统一执行器
在 Java 侧新增 `SkillSandboxExecutor` 作为 Skill 脚本执行的统一入口。调用方只需构造一个 `SandboxRequest` 即可:
第9步:构造沙箱执行请求
通过 Builder 模式构造请求,指定脚本路径、Skill 目录、参数以及只读根目录:
SkillSandboxExecutor.SandboxRequest sandboxReq = SkillSandboxExecutor.SandboxRequest.builder()
.script(scriptFile.toPath().toAbsolutePath())
.skillDir(skillDir.toAbsolutePath())
.args(List.of(pdfAbs.toString()))
.readRoots(List.of(pdfAbs.getParent()))
.extraEnv(Map.of("PYTHONIOENCODING", "utf-8"))
.useUvProject(true)
.build();
第10步:执行并处理结果
调用 `sandboxExecutor.execute(sandboxReq)` 执行脚本,并检查返回的 `SandboxResult`。如果 `isTimedOut()` 为真,说明执行超时;如果 `getExitCode() != 0`,说明执行失败。正常执行时,通过 `result.getStdout()` 获取标准输出。
第五阶段:验证与排错
通过一系列测试用例验证沙箱的有效性,包括良性脚本执行、联网拦截、越权写文件拦截、输出截断和超时处理。以下是常见问题及解决方法:
第11步:排查常见接入问题
- 报 python 找不到:检查 `envAllowlist` 是否包含 `PATH`,或本机是否已安装 Python。
- Windows 上内存/CPU 上限不生效:这是正常现象,`rlimit` 仅在 POSIX 系统生效,但超时和输出上限仍然有效。
- 输出被截断告警:脚本输出过大,可适当调大 `max-output-bytes` 配置。
- 用 uv 报找不到 pyproject.toml:确保 Skill 目录中包含 `pyproject.toml` 文件。
总结
执行隔离沙箱通过环境白名单、审计钩子、资源限制和容器化隔离,构建了纵深防御体系。阶段一保证下载内容可信,阶段二保证恶意脚本跑不出沙箱。生产环境建议优先使用容器级硬沙箱,开发环境可使用进程级软沙箱。后续阶段三将聚焦于输出校验与供应链治理闭环。
以上就是Agent动态Skill供应链安全加固(二):执行隔离沙箱实战的详细内容,更多关于Agent动态Skill供应链安全加固的资料请关注本站其它相关文章!
