Skip to content

fix(web-dashboard): i18n types + reverse-proxy host allowlist#28578

Open
Abhishek21k wants to merge 3 commits into
NousResearch:mainfrom
Abhishek21k:fix/dashboard-i18n-types-and-host-allowlist
Open

fix(web-dashboard): i18n types + reverse-proxy host allowlist#28578
Abhishek21k wants to merge 3 commits into
NousResearch:mainfrom
Abhishek21k:fix/dashboard-i18n-types-and-host-allowlist

Conversation

@Abhishek21k

Copy link
Copy Markdown

Summary

Two small, independent web-dashboard fixes:

  1. web/src/i18n/types.ts — declare the new scheduled kanban column key as optional so tsc -b accepts en.ts without forcing a 15-locale translation pass.
  2. hermes_cli/web_server.py — add an opt-in env var HERMES_DASHBOARD_ALLOWED_HOSTS so the DNS-rebinding Host-header defence can be paired with a reverse proxy or VPN-fronted tunnel without falling back to --insecure.

Each fix is its own commit; docs blurb is a third commit. The env var defaults to unset, so behaviour is unchanged for existing users.

Bug 1 — i18n types missing the scheduled key

web/src/i18n/en.ts adds a scheduled entry to both columnLabels (line 661) and columnHelp (line 671), but web/src/i18n/types.ts (lines 666-683) wasn't updated. tsc -b fails:

src/i18n/en.ts(661,7): error TS2353: Object literal may only specify
known properties, and 'scheduled' does not exist in type
'{ triage: string; todo: string; ready: string; running: string;
   blocked: string; done: string; archived: string; }'.
src/i18n/en.ts(671,7): error TS2353: Object literal may only specify
known properties, and 'scheduled' does not exist in type ...

Declaring the key as optional (scheduled?: string;) is the minimum-diff fix: en.ts now type-checks, and the 15 other locale files (af.ts, de.ts, es.ts, fr.ts, ga.ts, hu.ts, it.ts, ja.ts, ko.ts, pt.ts, ru.ts, tr.ts, uk.ts, zh-hant.ts, zh.ts) remain valid without a coordinated translation pass. They fall back to the English label, matching the existing i18n behaviour for missing keys.

Bug 2 — Host header rejects reverse-proxied access

Repro

  1. Start the dashboard bound to loopback (the default): hermes web.
  2. Front it with a reverse proxy on a different host (e.g. an internal hostname dashboard.internal terminating at the loopback bind).
  3. Open https://dashboard.internal/ in a browser.

The browser sends Host: dashboard.internal. _is_accepted_host only accepts localhost, 127.0.0.1, ::1 when bound to loopback, so the request is rejected with HTTP 400:

{"detail":"Invalid Host header. Dashboard requests must use the
 hostname the server was bound to."}

Why --insecure isn't a real workaround

--insecure requires --host 0.0.0.0, which:

  • Disables the DNS-rebinding defence entirely — every Host header is accepted on every interface. The whole point of the validator is gone.
  • Pushes trust to the network layer — every interface is now reachable, so firewall / segmentation rules become the only barrier. That's a strictly worse posture than "loopback-only plus one named proxy host".

Fix

Add an env var HERMES_DASHBOARD_ALLOWED_HOSTS (comma-separated, case-insensitive) checked inside _is_accepted_host right after the host is normalised. Default unset → no change. When set, the named hosts are accepted in addition to the bound interface; all other hosts are still rejected.

HERMES_DASHBOARD_ALLOWED_HOSTS=dashboard.example.com,dashboard.internal hermes web

The bind stays on loopback, the DNS-rebinding defence stays on for every host except the operator-named ones, and the reverse proxy / tunnel works.

Backward compatibility

HERMES_DASHBOARD_ALLOWED_HOSTS unset → behaviour identical to today. No existing deployment is affected.

Test plan

  • npx tsc -b in web/ passes after the types.ts change (failed before).
  • _is_accepted_host("localhost", "127.0.0.1") still returns True (loopback alias path unchanged).
  • _is_accepted_host("example.com", "127.0.0.1") returns False when env is unset (rebinding defence intact).
  • _is_accepted_host("example.com", "127.0.0.1") returns True when HERMES_DASHBOARD_ALLOWED_HOSTS=example.com is exported.
  • _is_accepted_host("evil.test", "127.0.0.1") returns False even with HERMES_DASHBOARD_ALLOWED_HOSTS=example.com set (only listed hosts are allowed).
  • Case-insensitive: HERMES_DASHBOARD_ALLOWED_HOSTS=Example.COM matches Host: example.com.
  • Port stripping still works on the allowlist path (Host: example.com:8443 matches example.com).
  • Empty / whitespace entries in the env var (a,, ,b) don't accidentally allow empty Host headers.
  • Docs page renders the new subsection in the expected place (under the Security warning, above /reload).

Adding the new `scheduled` kanban column to `en.ts` (in both
`columnLabels` and `columnHelp`) broke `tsc -b` because the matching
shape in `types.ts` didn't list the key:

    src/i18n/en.ts(661,7): error TS2353: Object literal may only
    specify known properties, and 'scheduled' does not exist in type
    '{ triage: string; todo: string; ready: string; ... }'.

Declared as optional so the 15 other locale files (af, de, es, fr, ga,
hu, it, ja, ko, pt, ru, tr, uk, zh, zh-hant) remain valid without a
bulk-translation pass — they fall back to the English label, matching
existing i18n behaviour for missing keys.
`_is_accepted_host` rejects any Host header that doesn't match the
bound interface (or a loopback alias when bound to loopback). That
defence is correct for direct browser-to-loopback access, but it
makes the dashboard unreachable behind a reverse proxy or VPN-fronted
tunnel: the proxied request arrives on loopback while the browser
sends `Host: <public-hostname>`, returning HTTP 400.

Today the only escape is `--insecure --host 0.0.0.0`, which disables
the DNS-rebinding defence entirely AND requires network-layer trust
on every interface.

Add an opt-in env var `HERMES_DASHBOARD_ALLOWED_HOSTS` (comma-
separated, case-insensitive) checked inside `_is_accepted_host`
right after the host is normalised. When unset, behaviour is
unchanged; when set, the named hosts are accepted in addition to
the bound interface, preserving the rebinding defence for every
other Host value.
Adds a short "Reverse-proxy / VPN host allowlist" subsection under
the Security warning explaining the new env var: its purpose,
comma-separated format, an example invocation, and that the default
(unset) preserves existing direct-browser-to-loopback behaviour.
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard javascript Pull requests that update javascript code labels May 19, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Multiple competing PRs for the reverse-proxy host allowlist: #25173, #27113, #20136. This PR bundles the fix with an unrelated i18n type fix. Consider splitting or closing in favor of existing PRs.

@StartupBros

Copy link
Copy Markdown

Hey @Abhishek21k — the duplicate-detector bot grouped several dashboard-host-allowlist PRs together; flagging one thing for your awareness.

Two observations on this PR specifically:

  1. The HERMES_DASHBOARD_ALLOWED_HOSTS check runs before the bind discrimination, so the env var also widens explicit non-loopback binds (e.g. my-server.corp.net), not just the loopback case the docstring describes. Tightening this to only consult extras inside the loopback-bind branch would match the docstring's intent.
  2. The PR bundles unrelated i18n changes (scheduled? column on the schedule table). CONTRIBUTING.md asks for one logical change per PR; splitting the i18n piece out would make this easier to merge.

I opened #29195 covering the host-allowlist surface with the loopback-only placement and 7 regression tests. Let me know if it'd help to consolidate.

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 javascript Pull requests that update javascript code P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants