fix(plugins): apply truthy env semantics to project-plugin gate (#29156)

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.
This commit is contained in:
xxxigm 2026-05-20 19:58:50 +07:00 committed by Teknium
parent 11e6dd3c60
commit 09f85f2cf7

View file

@ -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: