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: