先把当时的情况摆出来。给编码助手接了一个自写的 MCP server,工具里用 read_file 直接读任意路径,测试时让它读 /etc/shadow 也成功了,只是被系统权限挡住报错,换成项目外的配置目录则完整返回内容。

真正麻烦的地方在于,几个原因的现象几乎一样。工具实现没有限定根目录,入参路径直接拼到 open 上。另一种情况是没有做路径规范化,../ 可以跳出允许目录。

不容易想到的是,MCP server 以当前用户身份运行,继承了全部读权限。还有一种情况,工具描述没写限制,模型会主动尝试越界路径。

复现一次越界读取

python -c "print(open('/etc/hostname').read())"

然后用 realpath 做根目录校验

python -c "import os;root=os.path.realpath('/srv/kb');p=os.path.realpath('/srv/kb/../etc/passwd');print(p.startswith(root))"

接着把允许目录写成白名单并在工具入口拦截

python -c "print({'allow_roots':['/srv/kb'],'deny':['..','/etc','~/.ssh']})"

剩下的就是用低权限用户运行 MCP server

docker run --user 10001:10001 -v /srv/kb:/data:ro mcp-fs:latest

恢复的判定其实很直接。越界路径一律返回拒绝,白名单内的读取正常,容器内以只读方式挂载,模型再尝试跳目录也不会拿到内容。

MCP 工具的能力上限就是进程的权限上限,路径校验和降权运行要一起做,只靠提示词说不要越界没有意义。拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。

作者 9823189