PicoBot/src/command/handlers/list_todos.rs
oudecheng 58f461c953 fix: 修复 SSRF 重定向绕过 + 符号链接路径遍历 + TodoItemSummary 字段缺失
P0: src/tools/http_request.rs SSRF 重定向绕过
  - reqwest::Client 默认跟随最多 10 次重定向,is_private_host 仅检查初始 URL
  - 攻击者可用公网 URL 返回 302 → http://127.0.0.1/http://169.254.169.254/(云元数据端点)绕过防护访问内网
  - 修复:.redirect(reqwest::redirect::Policy::none()) 完全禁用重定向

P1: src/tools/file_read/write/edit.rs 符号链接路径遍历
  - resolve_path 用 starts_with 检查但未 canonicalize
  - 攻击者可在 allowed_dir 内创建指向 /etc/passwd 的符号链接绕过限制
  - 修复:对 resolved 和 allowed 均执行 canonicalize 后比较
  - file_read: 文件必须存在,canonicalize 失败直接报错
  - file_write/edit: 文件可能不存在,降级到父目录 canonicalize

P1: src/protocol/mod.rs + list_todos.rs TodoItemSummary 字段缺失
  - 后端 TodoItemSummary 仅返回 4 字段,前端期望 7 字段
  - 缺失 priority, created_at, updated_at,前端 TodoPanel 无法显示
    优先级和时间戳
  - 修复:struct 补齐 3 字段,list_todos 构造时传递完整字段
2026-07-08 16:23:09 +08:00

94 lines
2.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::command::context::CommandContext;
use crate::command::handler::{CommandHandler, CommandMetadata};
use crate::command::response::{CommandError, CommandResponse};
use crate::command::Command;
use crate::protocol::TodoItemSummary;
use crate::storage::SessionStore;
use async_trait::async_trait;
use std::sync::Arc;
pub struct ListTodosCommandHandler {
store: Arc<SessionStore>,
}
impl ListTodosCommandHandler {
pub fn new(store: Arc<SessionStore>) -> Self {
Self { store }
}
}
#[async_trait]
impl CommandHandler for ListTodosCommandHandler {
fn can_handle(&self, cmd: &Command) -> bool {
matches!(cmd, Command::ListTodos { .. })
}
fn metadata(&self) -> Option<CommandMetadata> {
Some(CommandMetadata {
name: "list_todos",
description: "列出当前 Todo 列表",
usage: "/list_todos",
})
}
async fn handle(
&self,
cmd: Command,
ctx: CommandContext,
) -> Result<CommandResponse, CommandError> {
let task_id = match cmd {
Command::ListTodos { task_id } => task_id,
_ => None,
};
// 子代理scope_key = task_id全局唯一与 todo_write 保持一致)
// 主代理scope_key = topic_id.unwrap_or(session_id)
let scope_key = if let Some(tid) = task_id.as_deref() {
tid.to_string()
} else {
ctx.topic_id
.as_deref()
.filter(|t| !t.is_empty())
.or(ctx.session_id.as_deref())
.ok_or_else(|| {
CommandError::new(
"MISSING_CONTEXT",
"Cannot list todos: no session_id or topic_id in command context",
)
})?
.to_string()
};
let records = self
.store
.list_todos(&scope_key)
.map_err(|e| CommandError::new("LIST_TODOS_ERROR", e.to_string()))?;
tracing::info!(
scope_key = %scope_key,
record_count = records.len(),
"list_todos handler: reading from store"
);
let summaries: Vec<TodoItemSummary> = records
.into_iter()
.map(|r| TodoItemSummary {
id: r.id,
content: r.content,
status: r.status,
priority: r.priority,
created_at: r.created_at,
updated_at: r.updated_at,
created_by_message_id: r.created_by_message_id,
})
.collect();
let todos_json = serde_json::to_string(&summaries)
.map_err(|e| CommandError::new("SERIALIZE_ERROR", e.to_string()))?;
Ok(CommandResponse::success(ctx.request_id)
.with_metadata("todos", &todos_json)
.with_metadata("todos_scope_key", &scope_key))
}
}