8.3 路径守卫与跨范围检测
概述
`path_guard.rs` 是 RAF 文件系统安全的基础设施。所有文件系统工具(`ReadFile`、`WriteFile`、`EditFile`、`RemovePath` 等)在执行前都必须通过 `resolve_safe()` 或 `resolve_safe_new()` 解析用户提供的路径。这两个函数同时完成三项职责:
1. **路径规范化**:将相对路径和绝对路径解析为规范化的 `PathBuf` 2. **目录穿越防护**:防止 `../` 逃逸出基础目录 3. **工作区范围检测**:判断规范化后的路径是否在 `WorkspaceScope.root` 内
// crates/framework/src/tools/path_guard.rs
/// 将用户提供的路径相对于基础目录解析,含目录穿越防护。
pub(crate) fn resolve_safe(
base_dir: &Path,
path: &str,
scope_root: Option<&Path>,
) -> Result<(PathBuf, ScopeStatus), AgentError>;
/// resolve_safe 的变体,允许路径尚不存在(用于写操作)。
pub(crate) fn resolve_safe_new(
base_dir: &Path,
path: &str,
scope_root: Option<&Path>,
) -> Result<(PathBuf, ScopeStatus), AgentError>;
ScopeStatus 枚举
/// 工具内部用 scope 检测结果
#[derive(Debug, Clone)]
pub(crate) enum ScopeStatus {
/// 路径在工作区范围内
InScope,
/// 路径在工作区范围外
OutsideScope,
/// 无 scope 设置,无需判断
NotApplicable,
}
impl ScopeStatus {
/// 转为 JSON 响应中的 "scope" 标签值
pub fn to_label(&self) -> &str {
match self {
ScopeStatus::InScope => "workspace",
ScopeStatus::OutsideScope => "outside_workspace",
ScopeStatus::NotApplicable => "none",
}
}
}
resolve_safe() — 读操作路径解析
`resolve_safe()` 用于需要路径已存在的操作(如 `ReadFile`、`ListFiles`、`InspectFile`):
pub(crate) fn resolve_safe(
base_dir: &Path,
path: &str,
scope_root: Option<&Path>,
) -> Result<(PathBuf, ScopeStatus), AgentError> {
// 1. 构建候选路径
let candidate = if Path::new(path).is_absolute() {
PathBuf::from(path) // 绝对路径直接使用
} else if path.is_empty() || path == "." {
base_dir.to_path_buf() // 空路径 = base_dir
} else {
base_dir.join(path) // 相对路径拼接
};
// 2. 规范化路径(解析符号链接、相对组件)
let canonical = candidate.canonicalize().map_err(|e| {
AgentError::ToolError(format!(
"Path does not exist or cannot be resolved: {}", e
))
})?;
// 3. 目录穿越检测
let canonical_base = base_dir
.canonicalize()
.unwrap_or_else(|_| base_dir.to_path_buf());
if !canonical.starts_with(&canonical_base) {
return Err(AgentError::ToolError("Path traversal denied".into()));
}
// 4. Scope 检测
let scope_status = match scope_root {
Some(root) => {
let canonical_root = root
.canonicalize()
.unwrap_or_else(|_| root.to_path_buf());
if canonical.starts_with(&canonical_root) {
ScopeStatus::InScope
} else {
ScopeStatus::OutsideScope
}
}
None => ScopeStatus::NotApplicable,
};
Ok((canonical, scope_status))
}
流程图
graph TD
A[输入: path + base_dir] --> B[构建候选路径]
B --> C[canonicalize 路径规范化]
C -->|失败| D[返回错误:<br/>路径不存在]
C -->|成功| E[canonicalize base_dir]
E --> F{canonical 在<br/>canonical_base 内?}
F -->|否| G[返回错误:<br/>路径穿越拒绝]
F -->|是| H{scope_root<br/>是否为 Some?}
H -->|None| I[返回 NotApplicable]
H -->|Some| J[canonicalize scope_root]
J --> K{canonical 在<br/>scope_root 内?}
K -->|是| L[返回 InScope]
K -->|否| M[返回 OutsideScope]
resolve_safe_new() — 写操作路径解析
`resolve_safe_new()` 用于路径可能尚不存在的操作(如 `WriteFile`、`MakeDirectory`、`MoveFile` 的源和目标):
pub(crate) fn resolve_safe_new(
base_dir: &Path,
path: &str,
scope_root: Option<&Path>,
) -> Result<(PathBuf, ScopeStatus), AgentError> {
let candidate = /* 同 resolve_safe 的候选构建 */;
// 手动处理 ".." 穿越(canonicalize 对不存在的路径无效)
let mut normalized = PathBuf::new();
for component in candidate.components() {
match component {
std::path::Component::ParentDir => {
if !normalized.pop() {
return Err(AgentError::ToolError("Path traversal denied".into()));
}
}
other => normalized.push(other),
}
}
// 尝试 canonicalize — 如果路径不存在,向上查找祖先
if let Ok(canon) = normalized.canonicalize() {
// 路径存在:直接验证
if !canon.starts_with(&canonical_base) {
return Err(AgentError::ToolError("Path traversal denied".into()));
}
let scope_status = match &canonical_scope_root {
Some(root) if canon.starts_with(root) => ScopeStatus::InScope,
Some(_) => ScopeStatus::OutsideScope,
None => ScopeStatus::NotApplicable,
};
return Ok((canon, scope_status));
}
// 路径不存在:向上遍历祖先直到找到存在的一个
let mut current = normalized.as_path();
loop {
match current.canonicalize() {
Ok(canon) => {
if !canon.starts_with(&canonical_base) {
return Err(AgentError::ToolError("Path traversal denied".into()));
}
let scope_status = match &canonical_scope_root {
Some(root) if normalized.starts_with(root) => ScopeStatus::InScope,
Some(_) => ScopeStatus::OutsideScope,
None => ScopeStatus::NotApplicable,
};
return Ok((normalized, scope_status));
}
Err(_) => match current.parent() {
Some(parent) => current = parent,
None => return Err(AgentError::ToolError(
"Path resolution failed: no existing ancestor found".into(),
)),
},
}
}
}
写操作祖先查找
当目标路径尚不存在时(例如 `WriteFile("/workspace/newdir/file.txt")` 但 `newdir/` 不存在),`resolve_safe_new()` 会沿着目录树向上遍历祖先:
Path: /workspace/a/b/c/new_file.txt
↑ 不存在
祖先查找:/workspace/a/b/c → 不存在
/workspace/a/b → 存在!
验证 /workspace/a/b 是否在 base_dir 内 → 是
验证 /workspace/a/b/c/new_file.txt 是否在 scope_root 内 → 用 starts_with 判断
目录穿越防护
两个函数都内置了目录穿越防护。以下攻击向量会被拦截:
// 向量 1:../ 逃逸
resolve_safe(Path::new("/workspace"), "../etc/passwd", None)
// → Error: Path traversal denied
// 向量 2:符号链接逃逸
// 假设 /workspace/link → /etc
resolve_safe(Path::new("/workspace"), "link/passwd", None)
// → 规范化后为 /etc/passwd,不在 /workspace 内 → Error
// 向量 3:绝对路径绕过
resolve_safe(Path::new("/workspace"), "/etc/passwd", None)
// → 绝对路径直接使用,规范化后不在 base_dir 内 → Error
工具中的使用模式
所有文件系统工具在 `execute()` 方法中遵循统一的使用模式:
// ReadFile 示例
async fn execute(&self, arguments: serde_json::Value) -> Result<ToolResult> {
let path = arguments["path"].as_str().unwrap_or("");
let base_dir = self.scope.as_ref()
.map(|s| s.root.as_path())
.unwrap_or_else(|| Path::new("."));
let (resolved, scope_status) = resolve_safe(
base_dir,
path,
self.scope.as_ref().map(|s| s.root.as_path()),
)?;
// 根据 ScopePolicy 决定是否拒绝
if let Some(scope) = &self.scope {
if scope.policy == ScopePolicy::DenyOutside
&& matches!(scope_status, ScopeStatus::OutsideScope)
{
return Ok(ToolResult::error(format!(
"Access denied: '{}' is outside workspace '{}'",
path, scope.name
)));
}
}
// 执行操作...
let content = std::fs::read_to_string(&resolved)?;
Ok(ToolResult::success(serde_json::json!({
"path": resolved.display().to_string(),
"scope": scope_status.to_label(),
"content": content,
})))
}
ScopeStatus 在工具响应中的体现
工具的 JSON 响应中始终包含 `scope` 字段:
// 工作区内操作
{
"path": "/workspace/src/main.rs",
"scope": "workspace",
"content": "fn main() { ... }"
}
// 工作区外操作
{
"path": "/etc/hosts",
"scope": "outside_workspace",
"content": "127.0.0.1 localhost"
}
`scope` 字段有两种用途: 1. **LLM 感知**:LLM 能看到操作是否越界,据此调整后续行为 2. **日志审计**:生产环境可以通过 `scope` 字段追踪 Agent 的越界行为
归纳
`path_guard.rs` 通过两个核心函数提供了三层防护:
| 层级 | 机制 | 实现方式 | |------|------|---------| | 路径规范化 | 解析符号链接、相对路径 | `Path::canonicalize()` | | 目录穿越防护 | 验证规范化路径在 base_dir 内 | `canonical.starts_with(canonical_base)` | | 工作区范围检测 | 判断路径是否在 WorkspaceScope.root 内 | `canonical.starts_with(canonical_root)` |
对于不存在的路径(写操作),`resolve_safe_new()` 通过祖先查找和手动 `..` 处理来保证安全性,确保即使目标路径尚未创建也不会被用于路径穿越攻击。