AI Agent 文件读写:Rust 工具先做路径规范化 AI Agent 文件读写Rust 工具先做路径规范化我最近在写一个能用文件作为上下文的 AI Agent。第一个版本的逻辑是Agent 告诉我要读哪个文件我就直接std::fs::read_to_string(path)。结果有一天在测试时我用了一个包含..的路径Agent 居然读到了一个完全不该访问的目录里的文件。那一瞬间我后背一凉——如果 Agent 被恶意提示词诱导说请读取~/.ssh/id_rsa我的代码是不是也会照做在自学的过程中我之前对路径安全的理解非常粗浅。我以为加上一个目录前缀就能防越界了后来才学到符号链接、../穿越、Unicode 欺骗这些攻击向量。今天这篇是我在 Rust 里写文件工具时整理的路径安全笔记核心就是一条原则在文件操作前先把路径规范化到规范形式再检查它是否还在允许的范围内。一、所有文件操作都从 Workspace Root 出发Agent 的文件能力必须绑定到一个明确的工作区根目录。任何用户输入和 Agent 生成的路径都要以这个根目录为锚点来做解析和检查flowchart TD A[用户/Agent 输入路径 User Input Path] -- B[拼接到 Workspace Root Join with Root] B -- C[调用 canonicalize 解析符号链接和 .. Resolve] C -- D{文件存在? File Exists} D --|不存在 Not Exist| E{是写操作? Write Operation} E --|否 No| F[拒绝目标文件不存在 Reject] E --|是 Yes| G[对父目录做安全检查 Check Parent Dir] D --|存在 Exists| H[检查是否为普通文件 Check Regular File] H -- I{在 Root 范围内? Inside Root} I --|是 Yes| J[允许操作 Allow] I --|否 No| K[拒绝路径越界 Reject Audit] G -- I style K fill:#f66,stroke:#333 style F fill:#f66,stroke:#333 style J fill:#6f6,stroke:#333这个流程的关键在于不能因为用户说路径是docs/intro.md就相信它只读了 docs 目录。canonicalize会解析所有符号链接和..穿越得到一个真实的物理路径然后再做前缀匹配才能真正防住越界。二、路径检查的核心实现 — 越界必须当场拦截下面是我现在每次文件操作前都会调用的检查函数use std::path::{Path, PathBuf}; /// 确保目标路径在 Workspace Root 内 /// 返回解析后的规范路径或返回越界/不存在等错误 fn ensure_inside_root(workspace_root: Path, user_input: Path) - ResultPathBuf, String { // 第一步将用户输入拼接到根目录上 let full_path workspace_root.join(user_input); // 第二步解析符号链接和 .. 等路径穿越 let canonical full_path.canonicalize().map_err(|e| { format!( 无法解析路径 {}原因: {}。请确认文件存在且路径正确, user_input.display(), e ) })?; // 第三步规范化根目录本身防止根目录本身是符号链接 let root_canonical workspace_root.canonicalize().map_err(|e| { format!(Workspace 根目录解析失败: {}, e) })?; // 第四步前缀匹配检查 — 这是防越界的最后一道门 if canonical.starts_with(root_canonical) { Ok(canonical) } else { // 审计记录越界访问尝试 eprintln!( [SECURITY] 路径越界尝试: 输入{} 解析后{} 根目录{}, user_input.display(), canonical.display(), root_canonical.display() ); Err(format!( 拒绝访问路径 {} 超出了工作区范围。请确认操作范围在 {} 内, user_input.display(), root_canonical.display() )) } }这个函数里canonicalize的两个特性非常重要1它会解析..和.这些相对路径符号2它会跟随符号链接返回真正的文件位置。如果文件不存在canonicalize会返回错误——对于写操作来说需要单独处理检查父目录是否在根目录内允许在安全范围内创建新文件。三、写入操作需要更严格的策略读文件越界已经很糟糕了但写文件越界可能覆盖关键配置、破坏仓库状态甚至写入恶意脚本。所以我给写操作单独定了一套策略/// 写入操作的安全检查策略 struct WritePolicy { /// 允许写入的目录白名单 allowed_dirs: VecString, /// 是否禁止操作隐藏文件.env, .git 等 deny_hidden_files: bool, /// 是否允许覆盖已存在的文件 allow_overwrite: bool, /// 单个文件的最大字节数 max_file_size: usize, } /// 检查写入目标是否满足安全策略 fn check_write_safety( path: Path, policy: WritePolicy, content: [u8], ) - Result(), String { // 1. 检查文件大小不能超过上限 if content.len() policy.max_file_size { return Err(format!( 文件内容 {} 字节超过上限 {} 字节, content.len(), policy.max_file_size )); } // 2. 检查是否允许操作隐藏文件 if policy.deny_hidden_files { if let Some(name) path.file_name().and_then(|n| n.to_str()) { if name.starts_with(.) { return Err(format!( 出于安全考虑禁止操作隐藏文件: {}, name )); } } } Ok(()) }AI Agent 生成的文件内容是不可预测的。最好默认限制大小、禁止覆盖关键文件、写入到专门的 output 目录用户确认后再移动到目标位置。多一道确认门槛就少一个意外事故。四、隐藏文件和特殊文件也需要防范有一类文件不应该被普通 Agent 任务接触.env里有密钥、.git/config里有仓库信息、socket 文件和设备文件更不应该被误当作文本打开。我在读取文件前会先调用metadata检查文件类型/// 确认目标是普通文件不是目录、符号链接、socket 或设备文件 fn ensure_regular_file(path: Path) - Result(), String { let metadata std::fs::metadata(path) .map_err(|e| format!(无法读取文件信息: {}, e))?; if !metadata.is_file() { return Err(format!( {} 不是普通文件可能是目录/符号链接/设备文件等禁止操作, path.display() )); } Ok(()) }这些检查写起来不复杂但能挡住很多意外的输入。Agent 收到的 prompt 是外部输入不能假设它永远是友好的。系统的安全性不能建立在用户是好意这个假设上。有一个容易被忽略的场景是文件大小。Agent 如果被要求读取一个几十 MB 的日志文件不加限制可能会把内存撑爆甚至把模型的上下文窗口直接塞满。我后来在读取函数里加了max_bytes参数超过上限就返回摘要并提示用户可以先截断再处理。小限制大作用。五、总结Rust 写 AI Agent 文件工具时必须在所有文件操作前先做路径规范化定义 workspace root 作为锚点、用canonicalize解析符号链接和路径穿越、检查最终路径是否在根目录内、对写入操作加更严格的策略限制。作为自学者我以前没意识到文件路径里有这么多安全细节。直到 Agent 真的读到了不该读的目录我才明白文件系统边界的严肃性。Agent 越能帮用户处理文件就越需要学会守规矩——范围清楚用户才敢让它处理真实项目的文件。