From 5367f19f1c5f3470ff8365aa6b9c355ac37bb8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sun, 14 Jun 2026 17:06:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20FR-S2=20read=5Ffile=20TOC?= =?UTF-8?q?TOU=20+=20write=5Ffile=20=E6=97=A0=E5=A4=A7=E5=B0=8F=E9=99=90?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - read_file: 单次 File::open 取 metadata+read 消除 TOCTOU(原 metadata+read 两步 syscall 间 symlink 替换破 1MB 限) - write_file: content.len() 超 1MB bail(原无限制,LLM 经审批可写超大文件撑爆磁盘/对话历史) 来源 fullstack-review §2 P0 安全(已核实);cargo 0 err --- src-tauri/src/commands/ai/tool_registry.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index c1f5c37..f180760 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -361,12 +361,18 @@ pub fn build_ai_tool_registry(db: &Arc) -> AiToolRegistry { args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?, )?; let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; - let metadata = tokio::fs::metadata(path).await + // 单次 File::open 取 metadata+read 消除 TOCTOU(FR-S2:原 metadata+read 两步 syscall 间 symlink 替换破 1MB 限) + use tokio::fs::File; + use tokio::io::AsyncReadExt; + let mut file = File::open(path).await .map_err(|e| anyhow::anyhow!("无法访问文件 {}: {}", path, e))?; + let metadata = file.metadata().await + .map_err(|e| anyhow::anyhow!("读取元数据失败 {}: {}", path, e))?; if metadata.len() > 1_048_576 { anyhow::bail!("文件超过 1MB 限制 ({} 字节)", metadata.len()); } - let content = tokio::fs::read_to_string(path).await + let mut content = String::new(); + file.read_to_string(&mut content).await .map_err(|e| anyhow::anyhow!("读取文件失败: {}", e))?; let result = if let Some(offset) = args["offset"].as_u64() { let lines: Vec<&str> = content.lines().collect(); @@ -406,6 +412,10 @@ pub fn build_ai_tool_registry(db: &Arc) -> AiToolRegistry { )?; let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; let content = args["content"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 content 参数"))?; + // 写入大小上限(FR-S2:原无限制,LLM 经审批可写超大文件撑爆磁盘/对话历史) + if content.len() > 1_048_576 { + anyhow::bail!("写入内容超过 1MB 限制 ({} 字节)", content.len()); + } if let Some(parent) = std::path::Path::new(path).parent() { tokio::fs::create_dir_all(parent).await .map_err(|e| anyhow::anyhow!("创建目录失败: {}", e))?;