From 09f85f2cf79362a2f7963754b49a44cb3d234176 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Wed, 20 May 2026 19:58:50 +0700 Subject: [PATCH] fix(plugins): apply truthy env semantics to project-plugin gate (#29156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GHSA-5qr3-c538-wm9j — half one of the bypass chain. ``_discover_dashboard_plugins`` opted into the untrusted ``./.hermes/ plugins/`` source via ``if os.environ.get("HERMES_ENABLE_PROJECT_ PLUGINS"):`` — which is True for any non-empty string. ``=0``, ``=false``, ``=no``, ``=off`` all return non-empty strings and so *enabled* the project source even though every operator (and the agent loader, ``hermes_cli/plugins.py`` line 815) reads those values as "disabled". An attacker who can land a manifest under the CWD's ``.hermes/plugins/`` directory — a malicious cloned repo, a worktree checked out from a forked PR, a CI runner workspace — was therefore guaranteed to get their manifest discovered the moment the user ran ``hermes dashboard`` from that directory, regardless of whether the user thought they had project plugins disabled. Switch to the shared ``utils.env_var_enabled`` helper used by the agent loader so the gate accepts the documented truthy set (``1`` / ``true`` / ``yes`` / ``on``, case-insensitive) and treats everything else — including ``0`` / ``false`` / ``no`` — as off. Half two (path-traversal + project-source ``api`` import) lands in the next commit. Together they break the RCE chain at two distinct choke points so a future regression in either one alone can't re-open the advisory. --- hermes_cli/web_server.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index d48466d9f0b..33a4f27fcfa 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -48,6 +48,7 @@ from hermes_cli.config import ( redact_key, ) from gateway.status import get_running_pid, read_runtime_status +from utils import env_var_enabled try: from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect @@ -4064,7 +4065,16 @@ def _discover_dashboard_plugins() -> list: (bundled_root / "memory", "bundled"), (bundled_root, "bundled"), ] - if os.environ.get("HERMES_ENABLE_PROJECT_PLUGINS"): + # GHSA-5qr3-c538-wm9j (#29156): the previous ``os.environ.get(...)`` + # check treated *any* non-empty string as truthy, so ``=0``, ``=false``, + # and ``=no`` — all of which the agent loader and operators correctly + # read as "disabled" — silently *enabled* the untrusted project source + # in the web server. Combined with the absolute-path RCE primitive on + # the manifest's ``api`` field (now patched below), this turned the + # opt-in into a sticky always-on switch. Use the shared truthy + # semantics (``1`` / ``true`` / ``yes`` / ``on``) so the gate matches + # ``hermes_cli/plugins.py`` and the documented user contract. + if env_var_enabled("HERMES_ENABLE_PROJECT_PLUGINS"): search_dirs.append((Path.cwd() / ".hermes" / "plugins", "project")) for plugins_root, source in search_dirs: