From 4e64d5af17203a29d2e791da1b76c1898d7e0128 Mon Sep 17 00:00:00 2001 From: FailSafe Researcher Date: Wed, 10 Jun 2026 08:26:31 -0700 Subject: [PATCH] fix: enforce workdir confinement in LocalFileManagerDriver to prevent path traversal _full_path() bypassed the workdir for absolute paths (os.path.isabs check returned the path unchanged) and had no boundary check after normpath resolved '..' sequences. Both vectors allowed reading/writing arbitrary files on the host filesystem. This change: - Removes the isabs short-circuit; every path is joined with workdir (stripping a leading '/' so os.path.join doesn't discard the base). - Adds a realpath-based boundary check: if the resolved path falls outside workdir_real, a ValueError is raised before any I/O is attempted. Signed-off-by: FailSafe Researcher --- .../file_manager/local_file_manager_driver.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/griptape/drivers/file_manager/local_file_manager_driver.py b/griptape/drivers/file_manager/local_file_manager_driver.py index 05162b0c5..877c8191a 100644 --- a/griptape/drivers/file_manager/local_file_manager_driver.py +++ b/griptape/drivers/file_manager/local_file_manager_driver.py @@ -49,11 +49,19 @@ def try_save_file(self, path: str, value: bytes) -> str: return full_path def _full_path(self, path: str) -> str: - full_path = path if os.path.isabs(path) else os.path.join(self.workdir, path.lstrip("/")) - # Need to keep the trailing slash if it was there, - # because it means the path is a directory. + # Always join with workdir; stripping a leading '/' prevents os.path.join + # from discarding the workdir when the caller supplies an absolute path. + full_path = os.path.join(self.workdir, path.lstrip("/")) + # Preserve trailing separator — it signals a directory vs. a file. ended_with_sep = path.endswith("/") full_path = os.path.normpath(full_path) + # Enforce workdir boundary: reject absolute-path bypasses and '../' escapes. + workdir_real = os.path.realpath(self.workdir) + full_path_real = os.path.realpath(full_path) + if not (full_path_real == workdir_real or full_path_real.startswith(workdir_real + os.sep)): + raise ValueError( + f"Path {path!r} resolves outside the working directory {self.workdir!r}" + ) if ended_with_sep: full_path = full_path.rstrip("/") + "/" return full_path