Skip to content

[security] fix(dashboard): validate WebSocket Host and Origin - #30221

Closed
Hinotoi-agent wants to merge 2 commits into
NousResearch:mainfrom
Hinotoi-agent:fix/dashboard-ws-host-origin-guard
Closed

[security] fix(dashboard): validate WebSocket Host and Origin#30221
Hinotoi-agent wants to merge 2 commits into
NousResearch:mainfrom
Hinotoi-agent:fix/dashboard-ws-host-origin-guard

Conversation

@Hinotoi-agent

@Hinotoi-agent Hinotoi-agent commented May 22, 2026

Copy link
Copy Markdown
Contributor

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 Host header 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-controlled Host and Origin when 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

  • Dashboard WebSocket Host/Origin validation bypass on /api/pty, /api/ws, /api/pub, and /api/events.
  • DNS-rebinding / cross-origin dashboard WebSocket admission gap where HTTP routes were protected but WebSocket upgrades were not.
  • The existing dashboard session-token requirement remains in scope and unchanged.

Before this PR

  • Dashboard HTTP requests with a rebinding-style Host header were rejected by host_header_middleware().
  • Dashboard WebSocket upgrades did not pass through that HTTP middleware.
  • WebSocket handlers checked the dashboard session token and client IP, but not Host or Origin.
  • A token-bearing WebSocket upgrade with Host: evil.example and Origin: http://evil.example could reach the WebSocket handler.

After this PR

  • WebSocket upgrades repeat the dashboard Host validation before accept().
  • Browser-supplied WebSocket Origin headers must also target the bound dashboard host when present.
  • Invalid WebSocket Host/Origin combinations close with 4403.
  • Valid loopback Host/Origin combinations continue to work.
  • Non-WebSocket HTTP behavior is unchanged.

Why this matters

The dashboard WebSocket routes are not only passive status channels. In particular, /api/pty bridges 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:

  • #13530 added/covered HTTP Host-header validation for normal dashboard HTTP requests.
  • This PR applies the same boundary to FastAPI WebSocket routes, which are not covered by HTTP middleware.
  • #25072 and #26265 focus on making dashboard WebSocket admission work correctly for intentional non-loopback/network binds.
  • This PR does not change the client-IP network-bind policy; it adds Host/Origin validation before WebSocket acceptance.

Both kinds of fixes are needed because HTTP middleware and WebSocket route handlers are separate enforcement points in FastAPI/Starlette.

Attack flow

  1. The dashboard is bound to loopback and has HTTP Host validation enabled.
  2. A browser is induced to initiate a WebSocket upgrade to the local dashboard through an attacker-controlled origin/host pattern.
  3. The request carries a valid dashboard session token through the WebSocket query string. This token availability is a precondition; the issue does not by itself disclose or mint a token.
  4. HTTP routes reject the mismatched Host, but WebSocket handlers do not run the HTTP middleware.
  5. The WebSocket route accepts the upgrade and exposes the corresponding dashboard channel.

Affected code

  • hermes_cli/web_server.py
    • _is_accepted_host() was already used for HTTP requests.
    • New _ws_host_origin_is_allowed() applies that boundary to WebSocket upgrades.
    • New _ws_request_is_allowed() combines Host/Origin validation with the existing client-IP check.
    • /api/pty, /api/ws, /api/pub, and /api/events now call the shared WebSocket guard before accepting.

Root cause

  • FastAPI HTTP middleware does not protect @app.websocket routes.
  • The dashboard had Host validation for HTTP but not for WebSocket upgrades.
  • The WebSocket handlers validated token and client IP only, leaving Host/Origin unchecked.

Severity framing

  • Issue: dashboard WebSocket Host/Origin validation bypass
  • Suggested severity: Medium / High hardening, depending on the deployment and token-exposure assumptions.
  • Preconditions: exploitation requires a token-bearing WebSocket request. This PR does not claim that the Host/Origin gap alone exposes the dashboard token or enables unauthenticated access.
  • Rationale: once the token precondition is satisfied, the WebSocket transport can bypass the dashboard Host/Origin boundary that already protects HTTP routes. The highest-impact route is /api/pty, because it bridges browser WebSocket input into the embedded dashboard PTY when that feature is enabled.
  • Maintainer-facing summary: this is a WebSocket-only bypass of the existing dashboard DNS-rebinding/Host-header defense, not a standalone unauthenticated RCE.

Safe reproduction steps

A safe local regression shape is included in tests/hermes_cli/test_web_server_host_header.py:

  1. Simulate the dashboard being bound to 127.0.0.1 by setting app.state.bound_host.
  2. Enable embedded dashboard chat for the WebSocket routes under test.
  3. Open /api/events with a valid session token and attacker-style headers:
    • Host: evil.example
    • Origin: http://evil.example
  4. Confirm the WebSocket closes with 4403.
  5. Repeat with a valid Host and hostile Origin and confirm it closes with 4403.
  6. Repeat with valid loopback Host/Origin and confirm the WebSocket can be accepted.

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:

session_token_present True token_len 43
http_bad_host_status 400
ws_bad_host_bad_origin accepted
ws_good_host_bad_origin accepted
ws_good_host_good_origin accepted
ws_good_host_no_origin accepted

After the patch, hostile WebSocket Host/Origin combinations close before acceptance while valid loopback usage still works:

session_token_present True token_len 43
http_bad_host_status 400
ws_bad_host_bad_origin rejected WebSocketDisconnect 4403
ws_good_host_bad_origin rejected WebSocketDisconnect 4403
ws_good_host_good_origin accepted
ws_good_host_no_origin accepted

Changes in this PR

  • Adds _ws_host_origin_is_allowed() for WebSocket Host/Origin validation.
  • Adds _ws_request_is_allowed() to combine Host/Origin validation with the existing WebSocket client-IP guard.
  • Applies the shared WebSocket guard to /api/pty, /api/ws, /api/pub, and /api/events.
  • Adds regression tests for bad WebSocket Host, bad WebSocket Origin, and valid loopback Host/Origin.

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

  • Loopback dashboard usage remains unchanged for valid loopback Host/Origin requests.
  • Missing WebSocket Origin remains allowed for non-browser clients such as the dashboard's internal sidecar publisher.
  • The existing public/all-interface bind behavior is preserved because _is_accepted_host() already treats 0.0.0.0 / :: as an explicit operator opt-in where Host-layer protection cannot provide the same boundary.
  • The patch is intentionally narrow and does not redesign dashboard session-token handling.

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 checking Origin on browser WebSocket handshakes blocks a second same-browser admission signal from drifting out of policy.

Type of change

  • Security fix
  • Regression tests
  • Documentation update
  • Refactor only

Test plan

Commands run locally:

uv run --extra dev --extra web python -m pytest tests/hermes_cli/test_web_server_host_header.py -q
uv run --extra dev --extra web ruff check hermes_cli/web_server.py tests/hermes_cli/test_web_server_host_header.py
python -m py_compile hermes_cli/web_server.py tests/hermes_cli/test_web_server_host_header.py
git diff --check

Observed result:

11 passed
All checks passed!

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.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/cli CLI entry point, hermes_cli/, setup wizard labels May 22, 2026
@teknium1

Copy link
Copy Markdown
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 _is_accepted_host() helper and covers all 4 WS endpoints.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants