Skip to content

fix(tools): contain file read/write/edit to workspace root by default#311

Open
Jiangrong-W wants to merge 1 commit into
HKUDS:mainfrom
Jiangrong-W:harness-fix/openharness-file-path-containment-consolidated
Open

fix(tools): contain file read/write/edit to workspace root by default#311
Jiangrong-W wants to merge 1 commit into
HKUDS:mainfrom
Jiangrong-W:harness-fix/openharness-file-path-containment-consolidated

Conversation

@Jiangrong-W

Copy link
Copy Markdown

Summary

  • What problem does this PR solve? The built-in file tools (read_file, write_file, edit_file) resolved a model-/LLM-supplied path to an absolute host path and then read or wrote it directly. Workspace containment was only enforced inside the is_docker_sandbox_active() branch (which calls validate_sandbox_path). The Docker sandbox is disabled by default (SandboxSettings.enabled = False), so in the default, unsandboxed configuration nothing kept these tools inside the repository root. A prompt-injected or mistaken model could therefore escape the workspace via an absolute path or .. traversal:

    • read_file is read-only, so PermissionChecker.evaluate auto-allows it after only the credential denylist / optional path rules — out-of-workspace reads of arbitrary UTF-8 host files succeeded with no prompt.
    • write_file / edit_file are gated by a permission prompt and edit-diff approval in the interactive UI, but the shipped non-interactive runners (run_task_worker, used for background teammates spawned with --task-worker, and run_print_mode) install a permission callback that always returns True and pass no edit_approval_prompt. On that auto-approved path, out-of-workspace writes and edits also succeeded.

    Affected sites (decisive sink per path):

    • read_filesrc/openharness/tools/file_read_tool.py:52 (raw = path.read_bytes())
    • write_filesrc/openharness/tools/file_write_tool.py:59 (path.write_text(arguments.content, encoding=\"utf-8\"))
    • edit_filesrc/openharness/tools/file_edit_tool.py (path.write_text(updated, ...), both the sandbox and non-sandbox branches)
  • What changed? Workspace containment is now enforced for read_file, write_file, and edit_file before any read or write, independent of the Docker sandbox and independent of the approval path, by reusing the repo's existing path-boundary check:

    • Added validate_workspace_path(path, cwd, metadata) in src/openharness/sandbox/path_validator.py — the always-on counterpart to validate_sandbox_path, which it reuses for the actual containment check (real-path resolution + ancestor check, already rejecting absolute-outside paths, .. escapes, and symlink escapes). Exported from src/openharness/sandbox/__init__.py.
    • Each file tool calls validate_workspace_path(...) right after path resolution. For write_file / edit_file the check runs before both the approval branch and the auto-approve branch, so it fails closed before the user is ever prompted and also holds on the interactive path.
    • Added FilesystemSettings (restrict_to_workspace: bool = True, allow_paths: list[str] = []) on Settings.filesystem (src/openharness/config/settings.py), surfaced into the engine tool_metadata in src/openharness/ui/runtime.py (including the task-worker and print-mode runtimes, so the auto-approve paths inherit containment).

    Containment is on by default and security-positive: out-of-workspace reads, writes, and edits are denied by default. It is backward compatible — no public API signature changes, validate_sandbox_path is untouched, the new helper is additive, and tools called without metadata (e.g. in tests) default to containment-on. Operators who need to reach outside the repository can opt out per session with filesystem.restrict_to_workspace = false, or scope extra roots with filesystem.allow_paths. In-workspace edits keep the existing approval-prompt behavior.

Validation

  • uv run ruff check src tests scripts
  • uv run pytest -q
  • cd frontend/terminal && npx tsc --noEmit (if frontend touched)

Test summary (added with this change, covering both affected tools):

  • tests/test_tools/test_file_tool_containment.pyread_file scope (5 tests): rejects absolute-outside, rejects .. escape, allows in-workspace, honors restrict_to_workspace=False opt-out, honors allow_paths extra root.
  • tests/test_tools/test_file_write_containment.pywrite_file / edit_file scope (10 tests): rejects .. escape and absolute-outside on the headless auto-approve path, rejects an escape even when an approval callback is set (and asserts the callback is never invoked — fails closed), plus in-workspace, opt-out, and allow_paths allow-cases for both tools.

Both new suites fail on the base commit (the escapes succeed) and pass after this change:

tests/test_tools/test_file_tool_containment.py .....                     [ 33%]
tests/test_tools/test_file_write_containment.py ..........               [100%]
15 passed

Run together with the adjacent tool / sandbox / config suites it is regression-clean (137 passed, 1 skipped). uv run ruff check passes on every file touched here. The frontend was not modified, so the TypeScript check has nothing to run for this PR. The full uv run pytest -q suite is left for a maintainer's green CI run.

Notes

…ault

The built-in file tools resolved model-/LLM-supplied paths to an absolute
host path and read, wrote, or edited them directly. Workspace containment
was only enforced when the optional Docker sandbox was active (disabled by
default), so in the default configuration a prompt-injected or mistaken
model could escape the repository root via absolute paths or ../ traversal.

This consolidates two related fixes for the same root cause (containment
only under Docker) across the whole file-tool family:

- read_file: read arbitrary UTF-8 host files outside the workspace,
  subject only to the narrow credential denylist.
- write_file / edit_file: in the default interactive UI these are gated by
  a permission prompt and edit-diff approval, but the shipped
  non-interactive runners (the --task-worker used for background agents,
  and print mode) install a permission callback that always returns True
  and supply no edit_approval_prompt; in that auto-approved path a model
  could overwrite or edit host files outside the repository.

Enforce workspace containment for read_file, write_file, and edit_file
before any read or write, independent of the Docker sandbox and of the
approval path, reusing the repo's existing path-boundary check
(validate_sandbox_path) via a new validate_workspace_path helper.
Containment fails closed before the user is prompted. It is on by default
and can be relaxed per session through the new filesystem settings
(restrict_to_workspace / allow_paths), surfaced to tools as execution
metadata. Backward compatible: tools default to containment-on when no
metadata is supplied.

Signed-off-by: christop <825583681@qq.com>
@Jiangrong-W
Jiangrong-W force-pushed the harness-fix/openharness-file-path-containment-consolidated branch from 3646bc5 to dc333b1 Compare June 19, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Built-in file tools (read/write/edit) are not contained to the workspace root by default

1 participant