Summary
The web dashboard (src/conductor/web/server.py) serves an HTTP API and a WebSocket endpoint for the local browser UI. The request path currently has no origin or host validation, and the optional auth token covers only part of the surface. This issue tracks hardening that surface to standard practice for a locally-bound developer server.
This is defense-in-depth work, not a behavior change: the dashboard should continue to work exactly as it does today for the intended local-browser use case.
Current gaps
-
No origin/host validation. WebDashboard._create_app builds the FastAPI app with no middleware, and no route validates Origin, Referer, or Host. A repo-wide search for add_middleware / CORSMiddleware / TrustedHostMiddleware returns nothing in the request path.
-
Content type is not constrained on mutating routes. Mutating endpoints parse their body regardless of the declared Content-Type.
-
Auth coverage is inconsistent. _gate_token_ok is applied to gate resolution only. Other mutating routes and the other WebSocket message types (dialog_message / dialog_decline, iteration_limit_response) are not covered by any equivalent check. Notably, iteration_limit_response resolves the max_iterations safety gate.
-
The token can't be used from the browser UI. CONDUCTOR_GATE_TOKEN is read from an Authorization header, but the dashboard sends gate responses over the WebSocket (stores/workflow-store.ts), and browsers cannot set request headers on a WebSocket handshake. So enabling the token today breaks the dashboard's own approve/reject buttons — there is no configuration that is both enabled and usable from the UI.
Proposed work
Item 1 is the highest-value change and subsumes most of the others; the rest are layered on top so no single check is load-bearing.
Notes
--web-port is documented with a fixed port (docs/cli-reference.md), so the listening port is frequently deterministic rather than OS-random.
src/conductor/web/replay.py builds a second FastAPI app with the same posture and should get the same treatment.
- Please keep the existing behavior for the normal local-browser flow — the dashboard and
conductor gate respond must both continue to work without extra setup.
Acceptance criteria
- Requests carrying a foreign
Origin are rejected on every route, including the WebSocket handshake.
- Requests with a
Host outside the bound loopback address/port are rejected.
- All mutating routes and all inbound WebSocket message types enforce the same auth policy.
- The dashboard UI and
conductor gate respond both work end-to-end with protection enabled by default.
- Regression tests cover each of the above in
tests/test_web/.
Summary
The web dashboard (
src/conductor/web/server.py) serves an HTTP API and a WebSocket endpoint for the local browser UI. The request path currently has no origin or host validation, and the optional auth token covers only part of the surface. This issue tracks hardening that surface to standard practice for a locally-bound developer server.This is defense-in-depth work, not a behavior change: the dashboard should continue to work exactly as it does today for the intended local-browser use case.
Current gaps
No origin/host validation.
WebDashboard._create_appbuilds the FastAPI app with no middleware, and no route validatesOrigin,Referer, orHost. A repo-wide search foradd_middleware/CORSMiddleware/TrustedHostMiddlewarereturns nothing in the request path.Content type is not constrained on mutating routes. Mutating endpoints parse their body regardless of the declared
Content-Type.Auth coverage is inconsistent.
_gate_token_okis applied to gate resolution only. Other mutating routes and the other WebSocket message types (dialog_message/dialog_decline,iteration_limit_response) are not covered by any equivalent check. Notably,iteration_limit_responseresolves themax_iterationssafety gate.The token can't be used from the browser UI.
CONDUCTOR_GATE_TOKENis read from anAuthorizationheader, but the dashboard sends gate responses over the WebSocket (stores/workflow-store.ts), and browsers cannot set request headers on a WebSocket handshake. So enabling the token today breaks the dashboard's own approve/reject buttons — there is no configuration that is both enabled and usable from the UI.Proposed work
/wshandshake — the WebSocket handshake is not covered by the browser's same-origin policy, so it needs the check explicitly rather than inheriting one. Reject requests whoseOriginis not the dashboard's own and whoseHostis not the bound loopback address and port.application/jsonon mutating routes.Item 1 is the highest-value change and subsumes most of the others; the rest are layered on top so no single check is load-bearing.
Notes
--web-portis documented with a fixed port (docs/cli-reference.md), so the listening port is frequently deterministic rather than OS-random.src/conductor/web/replay.pybuilds a second FastAPI app with the same posture and should get the same treatment.conductor gate respondmust both continue to work without extra setup.Acceptance criteria
Originare rejected on every route, including the WebSocket handshake.Hostoutside the bound loopback address/port are rejected.conductor gate respondboth work end-to-end with protection enabled by default.tests/test_web/.