coze-dev / coze-dev/coze-studio
Unrestricted SSRF in plugin tool execution allows any authenticated user to reach internal-only network services
- 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
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