MCP工具调用链存在路径穿越、命令注入等六类攻击面,文章给出可直接复制的防御代码示例,强调路径解析用Path.resolve()、子进程禁用shell=True等关键原则。
MCP(Model Context Protocol)服务器向 LLM 提供真实的工具:读取文件、执行命令、获取 URL。问题在于:这些工具的参数来自不受信任的内容——一个网页、一份文档、另一个模型。这是一块教科书级别的注入攻击面,而 MCP 服务器正在反复踩中同样的六个坑。下面逐一讲解,并给出可以直接复制使用的修复代码。
一个 read_file 工具执行 open(base + user_path),一旦模型被告知读取 ../../../../etc/passwd,就会泄露 /etc/passwd。符号链接和绝对路径可以绕过简单的前缀检查。
from pathlib import Path
def resolve_within(base: str, user_path: str) -> Path | None:
base_p = Path(base).resolve()
target = (base_p / user_path).resolve() # resolves .. and symlinks
if base_p not in target.parents and target != base_p:
return None # refuse, don't clamp
return target
返回 None 时拒绝访问。永远不要"清理"路径后继续执行——那正是漏洞重新敞开的路径。
subprocess.run(f"convert {name}", shell=True) 等于直接缴械。模型能影响的任何工具参数都处于攻击者控制之下。
subprocess.run(["convert", name], shell=False, timeout=10) # argv list, never a string
将用户实际可控的参数列入白名单;凡不在列表上的一律拒绝。
pickle.load、torch.load(2.6 之前的默认行为)、不带安全加载器的 yaml.load,以及 numpy.load(allow_pickle=True) 都会在加载时执行代码。这是 huntr 平台上 MCP/AI 漏洞中支付赏金最多的一类。
import yaml
yaml.safe_load(data) # not yaml.load
torch.load(f, weights_only=True) # not the default on old versions
# for models: prefer safetensors over pickle entirely
一个 fetch_url 工具会乐此不疲地请求 http://169.254.169.254/(云元数据端点)或 http://localhost:6379。在发起连接前先解析主机并阻断私有地址段。
import ipaddress, socket
def is_public(host: str) -> bool:
ip = ipaddress.ip_address(socket.gethostbyname(host))
return not (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved)
同时关闭重定向并限制响应体大小。
不要把密钥写在工具代码里,也不要把请求数据未经清洗就回传。每个输入的长度和类型都要有边界限制;一个 50MB 的"文件名"是攻击,不是文件名。
每次发版前跑一遍:
grep -rn "shell=True\|os.system\|subprocess.call(" src/
grep -rn "pickle.load\|yaml.load(\|torch.load(\|allow_pickle=True" src/
grep -rn "open(\|Path(\|send_file\|extractall" src/
然后向本地实例发射攻击载荷(../、169.254.169.254、带有 __reduce__ 的 pickle),确认每一个都被拒绝。
我从事 MCP 服务器和 AI/ML 包的安全审计工作。如果想要完整版本——经得起符号链接/绝对路径考验的路径守卫及其回归测试、SSRF 解析器、安全反序列化替换方案,以及可以打在自己服务器上的精确攻击载荷——我将其打包成了 MCP Server Security Hardening Kit($19)。上面这套发布前检查清单是免费的;套件里的代码才是让每一项检查都通过的实际实现。