version: commit 22275b1
Summary
Any authenticated user can create a custom plugin whose tool definition points at an arbitrary host, including hosts that are only reachable from inside the backend's own network (internal services, cloud metadata endpoints in a cloud deployment). Invoking the tool, including through the plugin editor's own "Test Run" / debug feature, which requires no admin approval and no bot binding, causes the coze-server backend to issue the HTTP request and return the full raw response body to the caller. There is no host allowlist, no private-IP/metadata-IP blocking, and no scheme restriction anywhere on this code path.
Details
A plugin's tool definition (server URL, path, HTTP method, headers, body) is entirely attacker-controlled at plugin-creation time - any workspace member can create a plugin (POST /api/plugin_api/register_plugin_meta, create_api) with any "Plugin URL" and any tool "Tool path" / "Request method".
When a tool is executed, buildHTTPRequest (backend/domain/plugin/service/tool/invocation_http.go:127) builds the request directly from that stored configuration:
func (h *httpCallImpl) buildHTTPRequest(ctx context.Context, args *InvocationArgs) (httpReq *http.Request, err error) {
tool := args.Tool
rawURL := args.ServerURL + tool.GetSubURL()
reqURL, err := h.buildHTTPRequestURL(ctx, rawURL, args)
...
httpReq, err = http.NewRequestWithContext(ctx, tool.GetMethod(), reqURL.String(), bytes.NewBuffer(bodyBytes))
...
}
and Do() sends it with a plain, unconfigured resty client (invocation_http.go:51,112):
var defaultHttpCli *resty.Client = resty.New()
...
httpResp, err := restyReq.Send()
There is no validation anywhere in this path (or in exec_tool.go, which calls into it) that args.ServerURL resolves to a public, non-internal address. No DNS-rebinding protection, no blocklist for RFC1918 / loopback / link-local ranges (which would include the cloud metadata address 169.254.169.254 on AWS/GCP/Azure), and no scheme restriction. The only network-allowlist concept anywhere in the codebase (CODE_RUNNER_ALLOW_NET) belongs to a completely different feature (the sandboxed code-runner) and is not consulted here.
The tool can be invoked without publishing the plugin or binding it to any bot, via the plugin editor's own debug endpoint:
POST /api/plugin_api/debug_api
which reaches getToolDebugPluginInfo (backend/domain/plugin/service/exec_tool.go:445) and then the same execute() / httpCallImpl.Do() path used by a live bot.
PoC
(available upon request)
Impact
Any authenticated user, with no admin role and no approval workflow, can use the plugin/tool feature to make the coze-server backend issue HTTP requests to arbitrary hosts, including internal-only services that are not reachable from outside the deployment's own network, and read the full raw response body. In a cloud deployment this includes the instance metadata service (169.254.169.254), which commonly exposes IAM/service-account credentials. Within this deployment it already allows reconnaissance and data disclosure from every other backing service (etcd, MinIO, Elasticsearch, MySQL admin ports, NSQ admin) that the coze-server container can reach but the end user should not be able to.
The request method is taken directly from the attacker-authored tool definition with no allowlist, so non-idempotent methods (POST/PUT/DELETE) against internal services are equally reachable in principle; this report only demonstrates and scores the confirmed read/response-reflection primitive.
version: commit 22275b1
Summary
Any authenticated user can create a custom plugin whose tool definition points at an arbitrary host, including hosts that are only reachable from inside the backend's own network (internal services, cloud metadata endpoints in a cloud deployment). Invoking the tool, including through the plugin editor's own "Test Run" / debug feature, which requires no admin approval and no bot binding, causes the coze-server backend to issue the HTTP request and return the full raw response body to the caller. There is no host allowlist, no private-IP/metadata-IP blocking, and no scheme restriction anywhere on this code path.
Details
A plugin's tool definition (server URL, path, HTTP method, headers, body) is entirely attacker-controlled at plugin-creation time - any workspace member can create a plugin (
POST /api/plugin_api/register_plugin_meta,create_api) with any "Plugin URL" and any tool "Tool path" / "Request method".When a tool is executed,
buildHTTPRequest(backend/domain/plugin/service/tool/invocation_http.go:127) builds the request directly from that stored configuration:and
Do()sends it with a plain, unconfigured resty client (invocation_http.go:51,112):There is no validation anywhere in this path (or in
exec_tool.go, which calls into it) thatargs.ServerURLresolves to a public, non-internal address. No DNS-rebinding protection, no blocklist for RFC1918 / loopback / link-local ranges (which would include the cloud metadata address169.254.169.254on AWS/GCP/Azure), and no scheme restriction. The only network-allowlist concept anywhere in the codebase (CODE_RUNNER_ALLOW_NET) belongs to a completely different feature (the sandboxed code-runner) and is not consulted here.The tool can be invoked without publishing the plugin or binding it to any bot, via the plugin editor's own debug endpoint:
which reaches
getToolDebugPluginInfo(backend/domain/plugin/service/exec_tool.go:445) and then the sameexecute()/httpCallImpl.Do()path used by a live bot.PoC
(available upon request)
Impact
Any authenticated user, with no admin role and no approval workflow, can use the plugin/tool feature to make the coze-server backend issue HTTP requests to arbitrary hosts, including internal-only services that are not reachable from outside the deployment's own network, and read the full raw response body. In a cloud deployment this includes the instance metadata service (
169.254.169.254), which commonly exposes IAM/service-account credentials. Within this deployment it already allows reconnaissance and data disclosure from every other backing service (etcd, MinIO, Elasticsearch, MySQL admin ports, NSQ admin) that the coze-server container can reach but the end user should not be able to.The request method is taken directly from the attacker-authored tool definition with no allowlist, so non-idempotent methods (POST/PUT/DELETE) against internal services are equally reachable in principle; this report only demonstrates and scores the confirmed read/response-reflection primitive.