stage #4593 onto master (v0.51.550) — document extension sidecar CSP support + CSP test coverage
santastabber: docs (README + EXTENSIONS.md) clarifying extension sidecar CSP behavior + test_issue1909_csp_enforcement / test_issue2901_csp_connect_extra assertions. Docs+tests only, no prod code. Co-authored-by: santastabber <santastabber@users.noreply.github.com>
This commit is contained in:
1 parent
cf16ad4bb5
commit
8f3f6ea67a
4 files changed
+74
-1
No files matched your search
@@ -341,7 +341,7 @@ Full list of environment variables:
|
||||
| `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace |
|
||||
| `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default |
|
||||
| `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication |
|
||||
| `HERMES_WEBUI_CSP_CONNECT_EXTRA` | *(unset)* | Optional space-separated `http(s)://` or `ws(s)://` origins to append to the report-only CSP `connect-src` directive for reverse-proxy or tunnel deployments |
|
||||
| `HERMES_WEBUI_CSP_CONNECT_EXTRA` | *(unset)* | Optional space-separated `http(s)://` or `ws(s)://` origins to append to the enforced and report-only CSP `connect-src` directives for trusted reverse-proxy, tunnel, or extension sidecar deployments |
|
||||
| `HERMES_WEBUI_SSE_CHUNKED` | *(unset)* | Set truthy (`1`/`true`/`yes`/`on`) to send SSE with `Transfer-Encoding: chunked`. Needed behind buffering reverse proxies (e.g. `jupyter-server-proxy`) that otherwise buffer the whole stream; harmless but unnecessary for directly-served deployments |
|
||||
| `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled |
|
||||
| `HERMES_WEBUI_EXTENSION_MANIFEST` | *(unset)* | Optional relative JSON manifest inside `HERMES_WEBUI_EXTENSION_DIR` listing bundled scripts/styles to inject; see [WebUI Extensions](docs/EXTENSIONS.md) |
|
||||
|
||||
@@ -25,12 +25,15 @@ Extensions can:
|
||||
- inject configured same-origin scripts before `</body>`
|
||||
- read a local JSON manifest that lists bundled scripts/styles to inject
|
||||
- call the normal WebUI APIs available to the browser session
|
||||
- call trusted local loopback sidecars directly from extension JavaScript when
|
||||
the browser Content Security Policy allows that origin
|
||||
|
||||
Extensions cannot, by themselves:
|
||||
|
||||
- bypass WebUI authentication
|
||||
- serve files outside the configured extension directory
|
||||
- load third-party scripts/styles through the built-in injection config
|
||||
- register new WebUI backend routes or proxy arbitrary sidecar/backend traffic
|
||||
- change Hermes Agent permissions, models, memory, or tools unless they call
|
||||
existing authenticated APIs that already allow those changes
|
||||
|
||||
@@ -125,6 +128,42 @@ These restrictions keep the existing Content Security Policy intact and avoid
|
||||
turning the extension hook into a third-party script loader. Invalid configured
|
||||
URLs are ignored rather than injected.
|
||||
|
||||
## Trusted local sidecars
|
||||
|
||||
Manifest-bundled extensions may integrate with a trusted local sidecar process,
|
||||
such as a desktop companion listening on `http://127.0.0.1:17787`. The injected
|
||||
extension JavaScript talks to that sidecar directly from the browser; Hermes
|
||||
WebUI does not proxy those requests and does not create extension-owned backend
|
||||
routes.
|
||||
|
||||
Loopback sidecar origins are already included in WebUI's enforced CSP
|
||||
`connect-src` directive:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:*
|
||||
http://localhost:*
|
||||
http://ipc.localhost
|
||||
ws://127.0.0.1:*
|
||||
ws://localhost:*
|
||||
```
|
||||
|
||||
The wildcard ports above cover any loopback port, including
|
||||
`http://127.0.0.1:17787`. For a trusted non-loopback origin that you explicitly
|
||||
control, append the exact origin with `HERMES_WEBUI_CSP_CONNECT_EXTRA` before
|
||||
starting WebUI:
|
||||
|
||||
```bash
|
||||
HERMES_WEBUI_CSP_CONNECT_EXTRA=https://companion.example.internal \
|
||||
HERMES_WEBUI_EXTENSION_DIR=/path/to/my-extension/static \
|
||||
HERMES_WEBUI_EXTENSION_MANIFEST=extensions.json \
|
||||
./start.sh
|
||||
```
|
||||
|
||||
`HERMES_WEBUI_CSP_CONNECT_EXTRA` accepts space-separated `http(s)://` or
|
||||
`ws(s)://` origins only. It rejects paths, directive injection, and invalid port
|
||||
numbers. Avoid wildcard or remote origins unless you fully control the target;
|
||||
extension JavaScript runs with the logged-in WebUI session's authority.
|
||||
|
||||
## Static file serving
|
||||
|
||||
When `HERMES_WEBUI_EXTENSION_DIR` points at an existing directory, files under
|
||||
|
||||
@@ -69,6 +69,20 @@ def test_enforcing_csp_honors_valid_extra_connect_origins(monkeypatch):
|
||||
) in policy
|
||||
|
||||
|
||||
def test_enforcing_csp_allows_trusted_loopback_sidecars_by_default(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_WEBUI_CSP_CONNECT_EXTRA", raising=False)
|
||||
|
||||
policy = _headers_from_security_helper()["Content-Security-Policy"]
|
||||
|
||||
assert "connect-src" in policy
|
||||
assert "http://127.0.0.1:*" in policy
|
||||
assert "http://localhost:*" in policy
|
||||
assert "http://ipc.localhost" in policy
|
||||
assert "ws://127.0.0.1:*" in policy
|
||||
assert "ws://localhost:*" in policy
|
||||
assert "http://127.0.0.1:17787" not in policy
|
||||
|
||||
|
||||
def test_enforcing_and_report_only_csp_share_validated_connect_extra(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_WEBUI_CSP_CONNECT_EXTRA", "https://metrics.example.com")
|
||||
|
||||
|
||||
@@ -36,6 +36,26 @@ def test_csp_connect_src_includes_valid_extra_origins(monkeypatch):
|
||||
) in policy
|
||||
|
||||
|
||||
def test_csp_connect_src_includes_explicit_trusted_sidecar_origin(monkeypatch):
|
||||
from server import Handler
|
||||
|
||||
monkeypatch.setenv(
|
||||
"HERMES_WEBUI_CSP_CONNECT_EXTRA",
|
||||
"http://127.0.0.1:17787 ws://127.0.0.1:17787",
|
||||
)
|
||||
|
||||
report_only = Handler.csp_report_only_policy()
|
||||
|
||||
assert "http://127.0.0.1:17787" in report_only
|
||||
assert "ws://127.0.0.1:17787" in report_only
|
||||
|
||||
from api.helpers import _build_csp_enforced_policy
|
||||
|
||||
enforced = _build_csp_enforced_policy()
|
||||
assert "http://127.0.0.1:17787" in enforced
|
||||
assert "ws://127.0.0.1:17787" in enforced
|
||||
|
||||
|
||||
def test_csp_connect_src_rejects_directive_injection(monkeypatch, caplog):
|
||||
from server import Handler
|
||||
|
||||
|
||||
Reference in new issue
Block a user