猪头少年 - 云南AI专家 - 云南独立开发者

Engineering note

安全的文件与命令执行:先划边界,再赋能力

这是「Agent 工程实战」的第 11 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。

本篇要解决的问题: 用工作区沙箱、命令白名单、超时和审计,限制 Agent 的副作用。

返回专题路线


本章定位:本章从路径沙箱和命令 allowlist 开始建立安全边界。你会学习路径规范化、符号链接、原子写入、进程输出和超时等真实工程问题。

建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。安全检查必须在程序执行层完成,不能把安全寄托在 system prompt 或模型自觉上。

本章导读

本章从路径沙箱和命令 allowlist 开始建立安全边界。你会学习路径规范化、符号链接、原子写入、进程输出和超时等真实工程问题。

本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。

11.1 路径沙箱

定义唯一根目录:

public sealed class WorkspacePathResolver
{
    private readonly string _root;

    public WorkspacePathResolver(string root)
        => _root = Path.GetFullPath(root);

    public string Resolve(string relativePath)
    {
        if (Path.IsPathFullyQualified(relativePath))
            throw new UnauthorizedAccessException("不允许使用绝对路径");

        var full = Path.GetFullPath(Path.Combine(_root, relativePath));
        if (!full.StartsWith(_root + Path.DirectorySeparatorChar,
                StringComparison.OrdinalIgnoreCase)
            && !string.Equals(full, _root, StringComparison.OrdinalIgnoreCase))
            throw new UnauthorizedAccessException("路径超出 workspace 范围");

        return full;
    }
}

真实项目还要考虑符号链接、大小限制、文件扩展名 allowlist 和并发写入。

11.2 命令 allowlist

不要把模型给出的整段字符串交给 shell。第一阶段可只允许固定程序:

var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "dotnet",
    "git",
    "rg"
};

更稳妥的做法是把命令封装成业务工具,例如 search_code,而不是暴露通用 execute。工具越接近业务意图,风险越容易控制。

11.3 进程工具的工程约束

需要支持:

  • 标准输出和标准错误同时读取,避免管道阻塞;
  • 非零退出码;
  • 超时后杀死进程树;
  • 输出上限,防止模型上下文被刷满;
  • 工作目录限制;
  • 环境变量最小化;
  • 审计日志和审批记录。

11.4 本章交付物

  • WorkspacePathResolver
  • ICommandPolicy
  • 默认拒绝、显式允许的命令策略;
  • 文件大小、输出长度和进程超时限制;
  • 安全测试:../secret.txt、绝对路径、超大输出、长时间进程。

11.5 路径安全不是字符串前缀判断那么简单

路径安全至少包含三个阶段:规范化、边界判断、资源类型判断。

规范化用于消除 a/../b、重复分隔符和相对路径差异;边界判断确认结果仍然位于 workspace;资源类型判断确认调用者要求的是文件还是目录。

还要考虑符号链接:一个位于 workspace 内的链接可能指向 workspace 外。高安全等级的实现需要解析真实路径,或者在打开文件时使用操作系统提供的安全选项。

11.6 写入操作要避免半成品

直接写目标文件时,进程中断可能留下半截内容。可以采用临时文件加原子替换:

写入 target.tmp
刷新并关闭 target.tmp
验证文件大小或 hash
替换 target

替换前还要记录原文件是否存在,必要时保留备份。对 Agent 来说,“写成功”应该意味着文件已经完整落盘,而不是 WriteAllTextAsync 没有立即抛异常。

11.7 进程输出的两个管道

如果同时重定向 stdout 和 stderr,却只读取 stdout,stderr 缓冲区写满后,子进程可能阻塞,主程序也会一直等待。正确做法是并行读取两个流,再等待进程结束。

结果中至少应包含:

{
  "exitCode": 0,
  "stdout": "...",
  "stderr": "",
  "timedOut": false
}

11.8 练习:安全攻击用例

不要只测试正常路径。准备以下用例:workspace 外的绝对路径、多个 ..、符号链接、生成大量输出的命令、永不退出的命令、返回非零退出码的命令。每个用例都要有预期状态和日志事件。

11.9 本章产出

安全不是给工具加一句系统提示词,而是让工具执行器在程序层拒绝非法动作。完成本章后,任何模型输出都不能绕过路径、命令和资源限制。

单篇实战作业

实践:实现路径解析测试和命令策略测试;把安全用例当成回归用例提交,而不是只在本地手工尝试。

建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。安全检查必须在程序执行层完成,不能把安全寄托在 system prompt 或模型自觉上。

章节复盘

复盘问题:任何模型输出都能否穿过路径、命令、输出大小和超时限制?如果不能,继续补执行层策略。

本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。


下一篇:第 12 篇《错误处理、超时与取消:让失败有边界》

如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。

Next step

不要停在单篇文章。

沿主专题继续阅读,查看同一问题从概念到实战的完整路径。

返回「Agent 工程实战」路线