[security] fix(dashboard): validate WebSocket Host and Origin - #30221
Closed
Hinotoi-agent wants to merge 2 commits into
Closed
[security] fix(dashboard): validate WebSocket Host and Origin#30221Hinotoi-agent wants to merge 2 commits into
Hinotoi-agent wants to merge 2 commits into
Conversation
Hinotoi-agent
force-pushed
the
fix/dashboard-ws-host-origin-guard
branch
from
May 23, 2026 09:07
9cb437e to
c91a3ff
Compare
Contributor
|
Salvaged in #31685 with your authorship preserved on both commits. CI on the original was failing on a 95-commit-stale branch (unrelated kanban_notify flakes + teardown noise on attribution/build jobs that produced no actual errors). Fresh rebase against current main: all 156 web_server tests pass + 199 broader dashboard tests pass. Code applied as-is — nothing to nit on, the design correctly reuses the existing Thanks for catching this — FastAPI HTTP middleware not running for WS upgrades is exactly the kind of half-applied-guard the existing GHSA-ppp5-vxwm-4cf7 protection left behind. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR hardens the dashboard WebSocket routes so they enforce the same Host-header boundary that already protects normal dashboard HTTP requests.
The existing dashboard HTTP middleware rejects DNS-rebinding-style requests whose
Hostheader does not match the interface the dashboard was bound to. FastAPI HTTP middleware does not run for WebSocket upgrades, so the chat/event WebSocket routes still accepted upgrades with an attacker-controlledHostandOriginwhen a valid dashboard session token was supplied.This does not remove the dashboard session-token requirement and should not be read as unauthenticated remote code execution. The issue is that WebSocket upgrades were a weaker transport boundary than HTTP requests: if a token-bearing WebSocket request is available through a browser-mediated chain, prior token exposure, or an intentionally exposed dashboard deployment, the WebSocket route could bypass the Host/Origin checks that normal dashboard HTTP requests enforce.
This patch adds a shared WebSocket Host/Origin preflight and applies it before
accept()on the dashboard WebSocket routes.Security issues covered
/api/pty,/api/ws,/api/pub, and/api/events.Before this PR
Hostheader were rejected byhost_header_middleware().HostorOrigin.Host: evil.exampleandOrigin: http://evil.examplecould reach the WebSocket handler.After this PR
accept().Originheaders must also target the bound dashboard host when present.4403.Why this matters
The dashboard WebSocket routes are not only passive status channels. In particular,
/api/ptybridges browser WebSocket input into an embedded Hermes TUI PTY when dashboard chat is enabled. If WebSocket upgrades do not enforce the same DNS-rebinding boundary as HTTP routes, the dashboard has inconsistent admission checks across transports.This PR keeps the existing session-token requirement intact, but prevents the WebSocket transport from being the weaker path around the dashboard's Host-header defense.
How this differs from related issue/PR
This is related to the dashboard Host-header hardening family, but it fixes a different transport boundary:
#13530added/covered HTTP Host-header validation for normal dashboard HTTP requests.#25072and#26265focus on making dashboard WebSocket admission work correctly for intentional non-loopback/network binds.Both kinds of fixes are needed because HTTP middleware and WebSocket route handlers are separate enforcement points in FastAPI/Starlette.
Attack flow
Host, but WebSocket handlers do not run the HTTP middleware.Affected code
hermes_cli/web_server.py_is_accepted_host()was already used for HTTP requests._ws_host_origin_is_allowed()applies that boundary to WebSocket upgrades._ws_request_is_allowed()combines Host/Origin validation with the existing client-IP check./api/pty,/api/ws,/api/pub, and/api/eventsnow call the shared WebSocket guard before accepting.Root cause
@app.websocketroutes.Severity framing
/api/pty, because it bridges browser WebSocket input into the embedded dashboard PTY when that feature is enabled.Safe reproduction steps
A safe local regression shape is included in
tests/hermes_cli/test_web_server_host_header.py:127.0.0.1by settingapp.state.bound_host./api/eventswith a valid session token and attacker-style headers:Host: evil.exampleOrigin: http://evil.example4403.4403.Expected vulnerable behavior
On vulnerable code, the HTTP route rejects a bad Host header, but a WebSocket route can still be accepted with the same attacker-controlled Host/Origin pattern when a valid dashboard session token is supplied.
The local vulnerable signal observed before the patch was:
After the patch, hostile WebSocket Host/Origin combinations close before acceptance while valid loopback usage still works:
Changes in this PR
_ws_host_origin_is_allowed()for WebSocket Host/Origin validation._ws_request_is_allowed()to combine Host/Origin validation with the existing WebSocket client-IP guard./api/pty,/api/ws,/api/pub, and/api/events.Files changed
hermes_cli/web_server.py— adds and applies the WebSocket Host/Origin guard.tests/hermes_cli/test_web_server_host_header.py— adds WebSocket regression coverage alongside the existing HTTP Host-header tests.Maintainer impact
Originremains allowed for non-browser clients such as the dashboard's internal sidecar publisher._is_accepted_host()already treats0.0.0.0/::as an explicit operator opt-in where Host-layer protection cannot provide the same boundary.Fix rationale
The safest boundary is to enforce the dashboard's Host validation at every transport admission point. Reusing
_is_accepted_host()keeps HTTP and WebSocket behavior consistent, while checkingOriginon browser WebSocket handshakes blocks a second same-browser admission signal from drifting out of policy.Type of change
Test plan
Commands run locally:
Observed result:
Disclosure notes
This PR is intentionally bounded to the WebSocket transport gap in the dashboard Host/Origin boundary. It does not claim unauthenticated remote code execution and does not change the existing dashboard session-token model. The issue is best understood as a WebSocket-only bypass of the dashboard's existing DNS-rebinding/Host-header defense when a token-bearing WebSocket request is available.
In practical terms, this prevents WebSocket routes from becoming the weaker dashboard transport if the token is available through a separate exposure path, browser-mediated flow, or intentionally exposed deployment. It does not assert that this patch alone fixes token exposure or dashboard authentication design.