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