From d9a4b5a5e5c35b0a9ab050814c47346997f87afd Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:11:29 -0700 Subject: [PATCH] fix: validate memory provider names before filesystem lookup and setup commands Strict charset allowlist (alnum + - _, max 64) on the {name} path param of the memory-provider config/setup endpoints. Prevents traversal-shaped names from reaching find_provider_dir(), and setup now 404s when neither a loadable provider nor a plugin manifest exists, so the command-running path is only reachable for discoverable plugins. Adds regression tests. --- hermes_cli/web_server.py | 24 ++++++++++++++++++++++++ tests/hermes_cli/test_web_server.py | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index b14b38b5a6a..e45d47848fb 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -4895,8 +4895,24 @@ def _write_memory_provider_config_values( save_env_value(env_key, secret) +_MEMORY_PROVIDER_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$") + + +def _require_valid_memory_provider_name(name: str) -> None: + """Reject provider names that could traverse outside the plugin dirs. + + ``name`` is interpolated into filesystem paths by ``find_provider_dir()`` + and gates which plugin manifest's setup commands run. A strict charset + allowlist (no path separators, no dots) makes traversal impossible + regardless of how the downstream lookup evolves. + """ + if not _MEMORY_PROVIDER_NAME_RE.fullmatch(name or ""): + raise HTTPException(status_code=404, detail=f"Unknown memory provider: {name}") + + @app.get("/api/memory/providers/{name}/config") async def get_memory_provider_config(name: str): + _require_valid_memory_provider_name(name) provider = _load_memory_provider(name) if provider is None: # Undeclared providers (e.g. builtin) have no config surface. Return an @@ -4907,7 +4923,14 @@ async def get_memory_provider_config(name: str): @app.post("/api/memory/providers/{name}/setup") async def setup_memory_provider(name: str, body: MemoryProviderSetupRequest): + _require_valid_memory_provider_name(name) provider = _load_memory_provider(name) + if provider is None and not _memory_provider_manifest(name): + # No discoverable plugin directory → nothing whose manifest could + # legitimately declare setup commands. Refuse before the + # command-running path. (provider may be None with a manifest present + # when its pip deps aren't installed yet — that's the setup use case.) + raise HTTPException(status_code=404, detail=f"Unknown memory provider: {name}") if provider is not None and body.values: try: _write_memory_provider_config_values(name, provider, body.values) @@ -4921,6 +4944,7 @@ async def setup_memory_provider(name: str, body: MemoryProviderSetupRequest): @app.put("/api/memory/providers/{name}/config") async def update_memory_provider_config(name: str, body: MemoryProviderConfigUpdate): + _require_valid_memory_provider_name(name) provider = _load_memory_provider(name) if provider is None: raise HTTPException(status_code=404, detail=f"Unknown memory provider: {name}") diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index d8d164d11ad..035dddffe4c 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -522,6 +522,23 @@ class TestWebServerEndpoints: assert resp.status_code == 404 + def test_memory_provider_endpoints_reject_traversal_names(self): + # Names with path separators / dots must never reach the filesystem + # lookup or the setup command path. 404 = rejected by the name guard; + # 405 = the router collapsed the dotted path onto a different route + # (equally safe — the handler never ran). + for bad in ("..", "..%2f..%2fetc", "a.b", "x/y", ".hidden", ""): + resp = self.client.get(f"/api/memory/providers/{bad}/config") + assert resp.status_code in (404, 405), (bad, resp.status_code) + resp = self.client.post( + f"/api/memory/providers/{bad}/setup", json={"values": {}} + ) + assert resp.status_code in (404, 405), (bad, resp.status_code) + resp = self.client.put( + f"/api/memory/providers/{bad}/config", json={"values": {}} + ) + assert resp.status_code in (404, 405), (bad, resp.status_code) + def test_post_memory_provider_setup_persists_values_without_activation(self): from hermes_cli.config import load_config, load_env