修复: FR-S2 read_file TOCTOU + write_file 无大小限制
- 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
This commit is contained in:
@@ -361,12 +361,18 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
|||||||
args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?,
|
args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?,
|
||||||
)?;
|
)?;
|
||||||
let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
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))?;
|
.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 {
|
if metadata.len() > 1_048_576 {
|
||||||
anyhow::bail!("文件超过 1MB 限制 ({} 字节)", metadata.len());
|
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))?;
|
.map_err(|e| anyhow::anyhow!("读取文件失败: {}", e))?;
|
||||||
let result = if let Some(offset) = args["offset"].as_u64() {
|
let result = if let Some(offset) = args["offset"].as_u64() {
|
||||||
let lines: Vec<&str> = content.lines().collect();
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
@@ -406,6 +412,10 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
|||||||
)?;
|
)?;
|
||||||
let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?;
|
||||||
let content = args["content"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 content 参数"))?;
|
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() {
|
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||||
tokio::fs::create_dir_all(parent).await
|
tokio::fs::create_dir_all(parent).await
|
||||||
.map_err(|e| anyhow::anyhow!("创建目录失败: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("创建目录失败: {}", e))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user