From 724b6601f3b1b1b7bfbd9d1aff53f6a7ba8fbb23 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sun, 8 Feb 2026 21:49:26 +0530 Subject: [PATCH] fix(security): restrict local workspace writes to configured root --- backend/app/core/config.py | 7 +++++++ backend/app/services/agent_provisioning.py | 20 +++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 02a40b2d..04cac6d3 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -31,6 +31,13 @@ class Settings(BaseSettings): cors_origins: str = "" base_url: str = "" + # Optional: local directory where the backend is allowed to write "preserved" agent + # workspace files (e.g. USER.md/SELF.md/MEMORY.md). If empty, local writes are disabled + # and provisioning relies on the gateway API. + # + # Security note: do NOT point this at arbitrary system paths in production. + local_agent_workspace_root: str = "" + # Database lifecycle db_auto_migrate: bool = False diff --git a/backend/app/services/agent_provisioning.py b/backend/app/services/agent_provisioning.py index ab0ea972..592b0a03 100644 --- a/backend/app/services/agent_provisioning.py +++ b/backend/app/services/agent_provisioning.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json import re from pathlib import Path @@ -181,9 +182,22 @@ def _ensure_workspace_file( ) -> None: if not workspace_path or not name: return - # `gateway.workspace_root` is sometimes configured as `~/.openclaw`. - # Expand user here to avoid creating a literal `./~` directory under the backend cwd. - root = Path(workspace_path).expanduser() + # Only write to a dedicated, explicitly-configured local directory. + # Using `gateway.workspace_root` directly here is unsafe (and CodeQL correctly flags it) + # because it is a DB-backed config value. + base_root = (settings.local_agent_workspace_root or "").strip() + if not base_root: + return + base = Path(base_root).expanduser() + + # Derive a stable, safe directory name from the (potentially untrusted) workspace path. + # This prevents path traversal and avoids writing to arbitrary locations. + digest = hashlib.sha256(workspace_path.encode("utf-8")).hexdigest()[:16] + root = base / f"gateway-workspace-{digest}" + + # Ensure `name` is a plain filename (no path separators). + if Path(name).name != name: + return path = root / name if not overwrite and path.exists(): return