文件与行号
internal/mcp/client.go:1024-1066(readHeaderFramedMessage),配合 sendRequest 的 res := <-done 裸阻塞
问题描述
fmt.Sscanf(parts[1], "%d", &contentLength) 解析 Content-Length 后,make([]byte, contentLength) 前无任何上限校验。两个失败模式(均经独立 subagent 实测复现):
- 内存 DoS:
Content-Length: 2000000000 → break 成立 → 2GB 分配实测 0.58ms 完成(Go 零初始化但 RSS 真实占用)→ ReadFull 永久阻塞等 body → 反复触发可 OOM
- panic + 永久死锁:
Content-Length: 281474976710657(1<<48+1)→ panic: makeslice: len out of range。panic 虽被 safego.Go recover(client.go:565),但 goroutine 在 done <- result(:575)之前就退出了——而调用方 sendRequest 的 ctx 取消分支是无 select 兜底的裸 <-done,即使 ctx 取消也永久阻塞,对应工具调用永不返回、goroutine 泄漏。
(注:负值不 panic——break 条件 contentLength >= 0 使负值继续扫 header 直到 EOF,仅静默忽略。)
触发场景
stdio MCP server 向 stdout 输出任意以非 { 开头的行(崩溃日志、LSP 风格 header)+ 畸形/恶意 Content-Length。MCP 生态随手 npx 第三方 server,供应链面宽。
readMessage 的 default 分支把所有非 { 开头行路由到该函数,路径可达。
修复建议
make 前加 if contentLength < 0 || contentLength > 16MB { return error }
- sendRequest 的
res := <-done 改为带超时/ctx select,消除 panic-after-recover 死锁面
严重程度
Medium(经独立 subagent 复核 + 最小程序实测两种失败模式)
文件与行号
internal/mcp/client.go:1024-1066(readHeaderFramedMessage),配合 sendRequest 的res := <-done裸阻塞问题描述
fmt.Sscanf(parts[1], "%d", &contentLength)解析 Content-Length 后,make([]byte, contentLength)前无任何上限校验。两个失败模式(均经独立 subagent 实测复现):Content-Length: 2000000000→ break 成立 → 2GB 分配实测 0.58ms 完成(Go 零初始化但 RSS 真实占用)→ ReadFull 永久阻塞等 body → 反复触发可 OOMContent-Length: 281474976710657(1<<48+1)→panic: makeslice: len out of range。panic 虽被 safego.Go recover(client.go:565),但 goroutine 在done <- result(:575)之前就退出了——而调用方 sendRequest 的 ctx 取消分支是无 select 兜底的裸<-done,即使 ctx 取消也永久阻塞,对应工具调用永不返回、goroutine 泄漏。(注:负值不 panic——break 条件
contentLength >= 0使负值继续扫 header 直到 EOF,仅静默忽略。)触发场景
stdio MCP server 向 stdout 输出任意以非
{开头的行(崩溃日志、LSP 风格 header)+ 畸形/恶意 Content-Length。MCP 生态随手 npx 第三方 server,供应链面宽。readMessage 的 default 分支把所有非
{开头行路由到该函数,路径可达。修复建议
make前加if contentLength < 0 || contentLength > 16MB { return error }res := <-done改为带超时/ctx select,消除 panic-after-recover 死锁面严重程度
Medium(经独立 subagent 复核 + 最小程序实测两种失败模式)