Skip to content

[code-review] mcp client Content-Length 无上限校验 — 2GB 内存 DoS + makeslice panic 后 sendRequest 裸 <-done 永久死锁 #182

Description

@topcheer

文件与行号

internal/mcp/client.go:1024-1066(readHeaderFramedMessage),配合 sendRequest 的 res := <-done 裸阻塞

问题描述

fmt.Sscanf(parts[1], "%d", &contentLength) 解析 Content-Length 后,make([]byte, contentLength)无任何上限校验。两个失败模式(均经独立 subagent 实测复现):

  1. 内存 DoSContent-Length: 2000000000 → break 成立 → 2GB 分配实测 0.58ms 完成(Go 零初始化但 RSS 真实占用)→ ReadFull 永久阻塞等 body → 反复触发可 OOM
  2. 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 分支把所有非 { 开头行路由到该函数,路径可达。

修复建议

  1. make 前加 if contentLength < 0 || contentLength > 16MB { return error }
  2. sendRequest 的 res := <-done 改为带超时/ctx select,消除 panic-after-recover 死锁面

严重程度

Medium(经独立 subagent 复核 + 最小程序实测两种失败模式)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions