MCP 是 API 和 AI agent 之间的桥梁,许多 AIGW 为此提供了根据 OpenAPI spec,将现存 API 转换成 MCP 的功能。然而大部分 AIGW 在实现该功能时并没有严格检查客户端的输入。某些输入不仅仅会触发网关的 bug,甚至可以直接攻击到后端服务。
MCP to RESTful API:漏洞的温床?
对 MCP 实现中的命令注入问题早已有人研究。
- 今年早些时候 mcp-package-docs 项目就爆过任意命令执行的高危漏洞:CVE-2025-54073。原因是开发者直接将客户端的输入拼接在 shell 命令里。
- 有人也发现许多 MCP server 有 SSRF 的风险。
- OWASP上也有一个 MCP Command injection 的专门页面
不过许多 MCP to RESTful API 的实现,还是难以避免的出现可供注入的漏洞。严格来说,它们并不是完全信任客户端的输入,多多少少有一些检查。但也许是因为 OpenAPI spec 和 HTTP 协议太复杂了,有些地方依然有着无人把守的缺口。接下来,让我带领大家游览一下这些缺口,看看有什么办法绕过高墙。
在评估安全性之前,有个前提:我们认为配置是可信的。毕竟如果用户把 host header 作为 header parameter 发布出去,那么攻击者可以通过它来设置任意 host header 就不是什么超出预期的事情。下面我们评估的漏洞,都严格假定攻击者无法操纵 OpenAPI spec 的内容。
潜在漏洞
MCP to RESTful API 转换通常是这样实现的:
- 开发者通过 OpenAPI spec 或类似的 spec 定义参数的名称、类型和位置。
- 网关将 spec 转换成 JSONschema,发布出去。
- 客户端了解到对应的 schema,结合用户的上下文,生成对应的 JSON,发送给网关。
- 网关拿到 JSON 后,根据 spec 转换成 HTTP 请求。
其中 HTTP 请求如下:
POST /path/$path_param?query_param=$query_param_value HTTP/1.1\r\n
Host: xxxx\r\n
Header_param: $header_param_value\r\n
Cookie: cookie_x;cookie_param=$cookie_param_value\r\n
\r\n
$body_param网关在转换的时候,就是将 path_param 之类的参数,用客户端发过来的 JSON 里面对应字段替换。
高风险
这里面最大的风险是,客户端发过来的 param 里面有 \r\n,那么就可以构造出任意请求。比如设置 path_param 的值为 HTTP/1.1\r\n...\r\nDELETE /admin,则得到的请求如下:
POST /path/ HTTP/1.1\r\n
...\r\n
DELETE /admin?query_param=$query_param_value HTTP/1.1\r\n
Host: xxxx\r\n同样在 header_param_value 里面发送 \r\n 也有类似的危害。
中风险
次一点的风险是,path_param 的值可以被设置成带 ../ 的,这样就可以是任意的路径。虽然没办法构造出不同的 method 和 header,但配合现有的接口(比如一个低权限的 DELETE /{user_id}/db/${db_id}),可以把它变成高权限的操作(比如 DELETE /admin/resources)。
在测试中,我发现有些 AIGW 会接受用户发过来的 JSON 里面所有的字段,哪怕这些字段没有在 spec 里面列出。这种问题会导致攻击者能够指定任意的 header,可以造成后端服务不可用(下文会说明如何操作)。
低风险
最后值得一提的是,不同位置的参数有不同的分隔符。如果 AIGW 没有检测这些分隔符,则攻击者也可以通过这种方式来注入额外的参数。尽管这种注入方式要比 header 位置的注入的危害小一些,但还算得上是一种风险。
- path 参数:
/|? - query 参数:
& - cookie 参数:
;
实际支持 cookie 参数的 AIGW 很少,而且即使注入了额外的 cookie,也没什么危害,所以我没有测试各个 AIGW 对它的过滤情况。
测试结果
在阐述了 MCP 转 RESTful API 的潜在攻击面后,我们对几个支持此功能的知名开源项目进行了测试,以检验其是否存在上述问题。测试对象包括 Higress、AgentGateway、litellm 和 Unla。选择标准为:高知名度、开源、文档明确提及支持 MCP 转 RESTful API,且在同一技术栈下选取最具代表性的一个。鉴于存在安全风险的项目较为普遍,未测试的商业版产品未必更安全。
Higress
Higress 的技术栈是 Go Wasm (业务代码)+ Envoy (底层框架)。
高风险:
- Higress 调用了
url.Parse来解析最终的 path,该函数会拒绝\r\n。 - Envoy 在执行请求时会拒绝 header 里面的
\r\n字符。
中风险:
- Envoy 在执行请求时会对含
/../的请求做 301 跳转,所以无法设置任意路径。 - Higress 的请求参数必须在配置中显式声明,无法插入未声明的 header
低风险:
- 没有检查 path 里面是否含有
/和?。所以可以在 path 里面注入分界符,如把DELETE /users/{user_id}/orders/{order_id}变成DELETE /users/1?c=/orders/2,或GET /users/{user_id}变成GET /users/1/orders/2。当然也可以在里面插入任意的 query 参数。 - 同样没有检查 query 参数里面是否有
&。 - 顺便一提,如果参数值里面有
\0,比如
curl -X POST http://localhost:8000/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_user","arguments":{"user_id":"ac","include_details":"a\0c"}}}'
会触发某些 wasm 代码执行路径,导致跳过参数替换,比如 /users/{user_id} 变成 /users/。
结论:Higress 存在低风险。
披露情况:已向 Higress 报告(https://github.com/alibaba/higress/issues/3266),截至报告撰写时,该问题尚未得到修复。
AgentGateway
AgentGateway 的技术栈是 Rust。
高风险:
- AgentGateway 使用的 Rust 库会拒绝 path 里的
\r\n。 - header 里的
\r\n同样会被拒绝。
中风险:
- 在执行请求时会对含
/../的请求做 301 跳转,所以无法设置任意路径。 - AgentGateway 会直接使用
tools/callarguments 里面的{"header":{...}}来构造最终发送给后端的请求,导致攻击者可以通过自己的 header 来覆盖由 agentgateway 设置的 header。比如使用自定义的 host 来覆盖 agentgateway 配置的 host。有一种攻击方向是通过设置一个较小的 Content-Type,将 body 从中间截断。如果 client 支持 HTTP1 pipeline,则截断的剩余部分会成为一个新的请求。不过,Rust 认为 HTTP1 pipeline 不安全,没有在 client 中支持,此路径无法利用。当然可以通过设置一个特别大的 Content-Type,迫使后端服务一直尝试读取直到超时为止。用这种方式可以快速消耗后端服务的连接数(通过 http2 可以做到在单条客户端连接不断发起请求,来持续消耗后端服务的连接),如果后端是传统的一个线程一个请求的 IO 模型,而且没有调整默认的单进程的最大线程数,可以打满后端的线程资源,造成后端不可用。
低风险:
- 没有检查 path 里面是否含有
/和?。所以可以在 path 里面注入分界符,如把DELETE /users/{user_id}/orders/{order_id}变成DELETE /users/1?c=/orders/2,或GET /users/{user_id}变成GET /users/1/orders/2。当然也可以在里面插入任意的 query 参数。 - 同样没有检查 query 参数里面是否有
&。
结论:AgentGateway 存在中风险。
披露情况:已向 AgentGateway 报告,项目方已确认并修复。
litellm
litellm 的技术栈是 Python。
高风险:
- litellm 里用到的 Python 库 httpx 会拒绝 path 里的
\r\n:httpx.InvalidURL: Invalid non-printable ASCII character in URL, '\r' at position 26. - litellm 只支持 path parameters 和 query parameters,tools/call 时不支持 header,所以不能测试这个。需指出的是,litellm 可以正常加载带 header parameters 的 OpenAPI spec,而且文档里也没有说不支持,甚至 tools/list 时也能列出 header parameters 的参数,但是实际上在代码里是没有写关于 header parameters 的实现的。我花了不少时间调试才发现了这一点。另外 litellm 没有做不同种类 parameters 的隔离,如果不同 parameters 间有同名的参数,比如 path var user_id 和 query var user_id,在加载 OpenAPI spec 时会报错。
中风险:
- 在执行请求时不会对含
/../做特殊处理,所以可以利用这个漏洞访问任意后端路径,如通过../admin来访问 /admin 接口。 - litellm 会检查入参是否在配置中。它的检查在全部四个测试对象里是最严格的,甚至要求入参类型和配置的类型一致,而不是简单地做一个 to string 的转换。
低风险:
- 没有检查 path 里面是否含有
/。所以可以把GET /users/{user_id}变成GET /users/1/orders/2。不过 litellm 有检查?。 - 无法通过
&在 query 里注入额外参数。
结论:litellm 存在中风险。
披露情况:已向litellm报告,项目方已确认并修复:https://github.com/BerriAI/litellm/pull/18597。
Unla
Unla 的技术栈是 Go。
高风险:
- 会拒绝 path 中的
\r\n。 - 会拒绝 header 里面的
\r\n字符。
(注意当输入包含 \r\n 时,输出会是
HTTP/1.1 202 Accepted
Content-Type: text/plain; charset=utf-8
Date: xxx
Content-Length: 69
Acceptedevent: message
data: {"jsonrpc":"2.0","id":xx,"result":null}
这种混合了 200 和 202 HTTP 状态码的响应。估计触发了什么异常路径)
中风险:
- 在执行请求时不会对含
/../做特殊处理,所以可以利用这个漏洞访问任意后端路径,如通过../admin来访问 /admin 接口。 - 请求参数必须在配置中显式声明,无法插入未声明的 header
低风险:
- 没有检查 path 里面是否含有
/和?。所以可以在 path 里面注入分界符,如把DELETE /users/{user_id}/orders/{order_id}变成DELETE /users/1?c=/orders/2,或GET /users/{user_id}变成GET /users/1/orders/2。当然也可以在里面插入任意的 query 参数。 - query 中的
&会被转义。
结论:Unla 存在中风险。
注意 Unla 如果不设置 responseBody template 则返回的响应为空。这样虽然用起来比较麻烦(不能直接使用返回的 JSON,必须配一个模板),但是避免了不少泄露敏感数据的风险,因为异常的响应无法在模板中渲染出来。不过这不能防治攻击者任意发起写请求(只要用户暴露了一个 DELETE 接口即可)。所以我还是维持中风险的评估。
披露情况:已向Unla报告,项目方已确认并修复。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。