[security] fix(workspace): restrict session workspaces to trusted roots (#416)
* fix(workspace): restrict session workspaces to trusted roots * fix: use boot-time DEFAULT_WORKSPACE instead of profile default for trusted workspace root _profile_default_workspace() reads the agent's terminal.cwd which may differ from the WebUI's configured workspace root. Use _BOOT_DEFAULT_WORKSPACE (which respects HERMES_WEBUI_DEFAULT_WORKSPACE for test isolation) to stay consistent with how new_session() seeds the initial workspace. * docs: v0.50.34 release — version badge and CHANGELOG --------- Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
@@ -214,6 +214,31 @@ def set_last_workspace(path: str) -> None:
|
||||
logger.debug("Failed to set last workspace")
|
||||
|
||||
|
||||
def resolve_trusted_workspace(path: str | Path | None = None) -> Path:
|
||||
"""Resolve and validate a workspace path under the WebUI's trusted workspace root.
|
||||
|
||||
The trusted root is the WebUI boot-time DEFAULT_WORKSPACE (respects
|
||||
HERMES_WEBUI_STATE_DIR for test isolation). Session creation/update and
|
||||
workspace-list mutations must stay within that root so callers cannot repoint
|
||||
a session to arbitrary filesystem locations outside the intended sandbox.
|
||||
|
||||
Note: _profile_default_workspace() reads the agent's terminal.cwd which may
|
||||
differ from the WebUI's configured workspace root — always use DEFAULT_WORKSPACE
|
||||
here to stay consistent with how new_session() seeds the initial workspace.
|
||||
"""
|
||||
root = Path(_BOOT_DEFAULT_WORKSPACE).expanduser().resolve()
|
||||
candidate = root if path in (None, "") else Path(path).expanduser().resolve()
|
||||
if not candidate.exists():
|
||||
raise ValueError(f"Path does not exist: {candidate}")
|
||||
if not candidate.is_dir():
|
||||
raise ValueError(f"Path is not a directory: {candidate}")
|
||||
try:
|
||||
candidate.relative_to(root)
|
||||
except ValueError:
|
||||
raise ValueError(f"Path is outside the trusted workspace root: {candidate}")
|
||||
return candidate
|
||||
|
||||
|
||||
def safe_resolve_ws(root: Path, requested: str) -> Path:
|
||||
"""Resolve a relative path inside a workspace root, raising ValueError on traversal."""
|
||||
resolved = (root / requested).resolve()
|
||||
|
||||
Reference in New Issue
Block a user