Skip to content

Controller and agent runtime accept spoofed Host (DNS rebinding); LOCAL_STUDIO_API_KEY breaks the browser UI #272

Description

@abm9111

Controller and agent runtime accept spoofed Host (DNS rebinding); LOCAL_STUDIO_API_KEY breaks the browser UI

Version: 2.1.0 (74ca85ea, main) · macOS 26.5.2, Apple Silicon, Bun 1.3.14, Node 26.5.0

Two related issues. The first is the vulnerability; the second is why the obvious mitigation isn't usable today.


1. No Host allowlist on the controller (:8080) or agent runtime (:8081)

The frontend already defends itself correctly — a spoofed Host gets 421 Misdirected Request / {"error":"Host is not allowed"}. Neither backend service does the same:

$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: evil.example' http://127.0.0.1:8080/status
200
$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: evil.example' http://127.0.0.1:8081/api/agent/runtime/status
200
$ curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: evil.example' http://127.0.0.1:4783/api/settings
421   # frontend does it right

Binding to 127.0.0.1 does not mitigate DNS rebinding: after the attacker's TTL expires and evil.example resolves to 127.0.0.1, the browser treats requests to the local port as same-origin, so CORS never applies. A server-side Host check is the only thing left, and these two services don't have one.

Impact. The agent runtime is the severe case — it exposes, unauthenticated:

  • POST /api/agent/turn — runs an agent turn with tool access
  • POST /api/agent/terminal/pty/open — opens a PTY

Pi runs with the full permissions of the host user, so a page the user merely visits can reach arbitrary code execution. On the controller, an attacker can create and launch recipes; combined with trust_remote_code defaulting to true, launching an attacker-chosen HF repo is itself code execution.

Suggested fix: apply the frontend's existing Host allowlist middleware to both services, defaulting to localhost/127.0.0.1 plus the configured bind host.


2. LOCAL_STUDIO_API_KEY makes the browser UI non-functional

Setting an API key is the natural mitigation for #1, but it is not usable, for two independent reasons.

(a) The client can never obtain the key. getApiKey() in frontend/src/lib/api/connection.ts:140 reads:

return process.env.LOCAL_STUDIO_API_KEY?.trim() || "";

That variable is not NEXT_PUBLIC_-prefixed, so it is not inlined into the client bundle. In the browser it is always "".

(b) The controller wouldn't accept it anyway. use-controller-events.ts builds the SSE URL as a query parameter:

const sseUrl = apiKey ? `${apiBaseUrl}/events?api_key=${encodeURIComponent(apiKey)}` : `${apiBaseUrl}/events`;

But extractAuthToken() in controller/src/http/security-middleware.ts only reads the Authorization: Bearer and x-api-key headers — there is no query-parameter path. Confirmed live:

$ curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:8080/status?api_key=$KEY"
401
$ curl -s -o /dev/null -w '%{http_code}\n' -H "x-api-key: $KEY" http://127.0.0.1:8080/status
200

So the ?api_key= branch is dead code — it cannot succeed even if the key were available client-side.

Observed result with a key set: the Status page renders Standby / offline / No model loaded / GPUs 0 / Controller logs 0 lines while a model is in fact loaded and serving. Every client-side call to the controller returns 401.

Note also that createMutatingAuthMiddleware gates all non-public paths, not only mutating ones despite its name — isPublicRequest exempts only OPTIONS and /health, and there is no isMutatingRequest check in the handler. So reads (/status, /events) are gated too. If that is intended, the name is misleading; if not, that's a third bug.

Suggested fix: accept ?api_key= for the EventSource path (which cannot send custom headers), and expose the key to the client deliberately — or, better, proxy /events through the frontend so the key stays server-side.


Workaround in the meantime

Users on a shared or untrusted network should treat the controller and agent runtime as unauthenticated local RCE surfaces and avoid browsing untrusted sites while the stack is running. Setting LOCAL_STUDIO_DEFAULT_TRUST_REMOTE_CODE=false removes one escalation path from the controller but does not address the agent runtime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions