fix: CSRF check fails behind reverse proxy on non-standard ports (#360)

* fix: CSRF check fails behind reverse proxy on non-standard ports

When serving behind a reverse proxy (e.g. Nginx Proxy Manager) on a
non-standard port like 8000, the browser sends
`Origin: https://example.com:8000` but the proxy forwards `Host: example.com`
(without the port). The existing CSRF check compared these as raw strings,
causing all POST requests to be rejected with 403.

This commit:
- Adds `_normalize_host_port()` to properly parse host:port pairs (incl. IPv6)
- Adds `_ports_match()` that treats absent port as equivalent to 80/443
- Adds `HERMES_WEBUI_ALLOWED_ORIGINS` env var for explicitly trusting origins
  when port normalization alone isn't sufficient (e.g. port 8000)
- Adds unit tests covering port normalization, allowlist, and rejection cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CSRF port normalization — scheme-aware, allowlist validation, 29 tests (#360)

api/routes.py:
- _normalize_host_port(): parse host:port including IPv6 bracket notation
- _ports_match(scheme, origin_port, allowed_port): scheme-aware — http absent=:80,
  https absent=:443; prevents cross-protocol false match (http://host:80 no
  longer passes for https://host:443 server)
- _allowed_public_origins(): parse HERMES_WEBUI_ALLOWED_ORIGINS env var;
  warn and skip entries missing scheme prefix
- _check_csrf(): extract origin scheme, pass to _ports_match; add origin_scheme

tests/test_sprint29.py: 29 new tests (5 from PR + 24 added in review)
- Unit tests for _normalize_host_port and _ports_match helpers
- Cross-protocol rejection (http vs https default ports)
- Explicit :80 / :443 same-origin pass
- Non-default port rejection
- Bug scenario with/without allowlist
- Comma-separated allowlist
- No-scheme allowlist warning
- Trailing-slash normalization

CHANGELOG.md: v0.50.16 entry; 900 tests total (up from 871)

---------

Co-authored-by: liangxu.5 <liangxu.5@bytedance.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-13 12:23:16 -07:00
committed by GitHub
parent 5bdeb93559
commit f948baceb6
3 changed files with 296 additions and 5 deletions

View File

@@ -5,6 +5,15 @@
---
## [v0.50.16] Fix CSRF check failing behind reverse proxy on non-standard ports (PR #360)
- **CSRF no longer rejects POST requests from reverse-proxied deployments on non-standard ports** (`api/routes.py`, fixes #355): When serving behind Nginx Proxy Manager or similar on a port like `:8000`, browsers send `Origin: https://app.example.com:8000` while the proxy forwards `Host: app.example.com` (port stripped). The old string comparison failed this as cross-origin. Two changes fix it:
- `_normalize_host_port()`: properly splits host:port strings including IPv6 bracket notation (`[::1]:8080`)
- `_ports_match(scheme, origin_port, allowed_port)`: scheme-aware port equivalence — absent port equals `:80` for `http://` and `:443` for `https://`. This prevents the previous cross-protocol confusion where `http://host` could incorrectly match an `https://host:443` server (security fix applied on top of the original PR)
- `HERMES_WEBUI_ALLOWED_ORIGINS` env var: comma-separated explicit origin allowlist for cases where port normalization alone isn't sufficient (e.g. non-standard ports like `:8000` where the proxy strips the port entirely). Entries without a scheme (`https://`) are rejected with a startup warning.
- **Security fix applied during review**: the original `_ports_match` treated both port 80 and port 443 as interchangeable with "absent port", which is scheme-unaware. An `http://host` origin would pass for an `https://host:443` server. Fixed by making the default-port lookup scheme-specific.
- 29 new tests in `tests/test_sprint29.py` (5 from PR + 24 added during review): cover scheme-aware port matching, cross-protocol rejection, unit tests for `_normalize_host_port` and `_ports_match`, allowlist validation, comma-separated origins, no-scheme allowlist warning, the bug scenario with and without the allowlist; 900 tests total (up from 871)
## [v0.50.15] KaTeX math rendering for LaTeX in chat and workspace previews (fixes #347)
- **LaTeX / KaTeX math now renders in chat messages and workspace file previews** (`static/ui.js`, `static/workspace.js`, `static/style.css`, `static/index.html`): Inline math (`$...$`, `\(...\)`) and display math (`$$...$$`, `\[...\]`) are rendered via KaTeX instead of displaying as raw text. Follows the existing mermaid lazy-load pattern: delimiters are stashed before markdown processing, placeholder elements are emitted, and KaTeX JS is loaded from CDN on first use — no KaTeX JS is loaded unless math is present.