coze-dev / coze-dev/coze-studio

Unrestricted SSRF in plugin tool execution allows any authenticated user to reach internal-only network services

Open
#2,711 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
21.6k
Forks
3.1k
PR merge metrics
No merged PRs in 30d

Description

version: commit 22275b1c2661d35344a7493cffe401e8cc61cf8e

### 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:

```go
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`):

```go
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.

Contributor guide

Open the contributing guide

Research direction

Start with backend/domain/plugin/service/tool/invocation_http.go, especially buildHTTPRequest and Do, then trace the POST /api/plugin_api/debug_api path through getToolDebugPluginInfo in backend/domain/plugin/service/exec_tool.go. Verify the execution and debug paths no longer allow attacker-controlled tools to reach internal or metadata services or return their responses, and add focused coverage for those paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.