Skip to content
81504d373f4e32206dc885d3c415e278

81504d373f4e32206dc885d3c415e278

gist.github.com • September 29, 2026

Save DReazer/81504d373f4e32206dc885d3c415e278 to your computer and use it in GitHub Desktop.

Vendor: MODSetter Product: SurfSense ( ) Affected version: 0.0.36 (verified on official image ghcr.io/modsetter/surfsense-backend:0.0.36 ) Component: FastAPI backend, MCP connector management Vulnerability class: OS Command Injection / Unsafe Process Spawn CWE: CWE-78 (OS Command Injection), CWE-250 (Execution with Unnecessary Privileges) Attack type: Remote Authentication required: Yes (any registered user) User interaction: None

SurfSense 0.0.36 lets any authenticated user pass an arbitrary command , args , and env to POST /api/v1/connectors/mcp/test . The backend spawns that process with stdio MCP transport and no command allowlist. The MCP handshake is not a gate: the process is created first, so a non-MCP binary still runs.

On the official Docker stack the spawned process runs as uid 0 (root) inside the backend container. That process inherits the API worker environment ( SECRET_KEY , database URL, Redis, Stripe/Daytona keys if configured).

A second route, POST /api/v1/connectors/mcp , persists the same server_config . The chat agent later respawns it when loading MCP tools.

CVSS 3.1: 8.8 High CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

AV:N — reachable over HTTP

AC:L — single JSON POST after login

PR:L — default AUTH_TYPE=LOCAL and REGISTRATION_ENABLED=TRUE

UI:N — no second user

S:U — impact is the backend process/container (self-hosted official compose)

C/I/A:H — arbitrary command execution as root in the API container

On a shared multi-tenant deployment the same bug is better scored with S:C (one tenant’s RCE reaches other tenants’ data and host secrets): CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H (9.9 Critical).

CVSS 4.0 (recommended for VulDB, self-hosted official Docker): 8.7 High CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N NVD calculator:

CVSS 4.0 (shared multi-tenant SaaS, subsequent-system High): 9.4 Critical CVSS:4.0/AV:N/A NVD calculator: hC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA: Https://nvd.nist.gov/vuln-metrics/cvss/v4-calculator?vector=AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Metric rationale (self-hosted 8.7):

surfsense_backend/app/routes/search_source_connectors_routes.py — test_mcp_server_connection() (approx. lines 2990–3044):

transport defaults to stdio

command , args , env are taken from the JSON body

no workspace RBAC check on /test

no allowlist of binaries or packages

is_cloud() does not disable stdio

surfsense_backend/app/agents/chat/multi_agent_chat/shared/tools/mcp/client.py — MCPClient.connect() (approx. lines 75–81):

User env is merged on top of the worker environment ( PATH , LD_PRELOAD , etc.).

Reproduction (self-hosted Docker 0.0.36)

Tested 2026-08-14 against ghcr.io/modsetter/surfsense-backend:0.0.36 with the official compose file from the 0.0.36 tree. Frontend/API via Caddy: .

1. Register and obtain a Bearer token

Use access_token from the login response.

2. Trigger process spawn

The payload is not an MCP server. It only writes a marker file and exits. That is enough to prove execution: the API still starts the process.

HTTP 200 body (handshake fails; command already ran):

An out-of-band HTTP callback to an attacker-controlled Python listener also confirms the spawned process ran (the MCP handshake still fails; the request arrives anyway):

Figure: Python HTTP listener output proving the command was executed.

3. Confirm execution inside the backend container

Expected: reject non-allowlisted command , or refuse stdio on hosted/self-hosted API workers. Actual: /bin/sh ran as root in the backend container.

No extra role, workspace membership, or MCP server is required. Cookie sessions need a valid Origin (CSRF middleware). Bearer / desktop tokens do not.

An attacker who can register or otherwise obtain a normal user token can:

Execute arbitrary OS commands as root in the SurfSense backend container.

Read process environment: SECRET_KEY (JWT/session signing), DATABASE_URL , Redis, optional cloud/API keys.

Forge sessions, dump or alter the application database, and persist via POST /api/v1/connectors/mcp so later agent turns respawn the payload.

On Docker Desktop / default compose, this is container root. Combined with a container escape or a sensitive host mount it becomes host compromise. Even without escape, the application secrets are sufficient to take over the SurfSense instance.

This is not “MCP tool misuse.” The defect is unsanitized process creation on a user-reachable HTTP route.

Disable stdio MCP transport on the hosted API, or allow only a fixed wrapper ( npx / uvx ) plus a package-name allowlist.

Do not accept a free-form command from the client. Persist a server-side enum, not argv.

Drop user-supplied env or allow only named keys; never os.environ.copy() + update(user_env) .

Put POST /connectors/mcp/test behind the same workspace permission as connector create ( connectors:create ), and rate-limit it.

Treat handshake failure as irrelevant to the security fix — the spawn is the sink.

Discovered and verified against SurfSense 0.0.36 (official GHCR image) during an authorized source-code review and local Docker reproduction.

Repository:

Release:

Image: ghcr.io/modsetter/surfsense-backend:0.0.36

Route: POST /api/v1/connectors/mcp/test

Related persist path: POST /api/v1/connectors/mcp

Extracted Entities

Attack Types (1)

Vulnerabilities (1)