Skip to content

Permission modes do not enforce path scope on file tools or shell expansion in bash #3007

@AlexMelanFromRingo

Description

@AlexMelanFromRingo

The documented permission modes (read-only, workspace-write, danger-full-access) gate tool calls by mode-rank only. They do not constrain the target of a permitted operation. As a result, an agent running in workspace-write mode can read, modify, or delete files anywhere on the host, not just inside the workspace.

This is reproducible from the current main (commit at pushed_at: 2026-05-06).

Findings

F1. bash_validation is not reachable from runtime

runtime/src/bash_validation.rs defines validate_command, validate_read_only, check_destructive, validate_paths, plus a full unit-test suite. runtime/src/lib.rs declares pub mod bash_validation;, but no other crate ever imports any symbol from it.

$ rg -n 'bash_validation::|validate_read_only|validate_command\b|check_destructive' rust/crates/ \
    | grep -v 'bash_validation.rs'
rust/crates/runtime/src/lib.rs:8:pub mod bash_validation;

bash::execute_bash runs the command directly via prepare_tokio_command -> sh -lc <command> without invoking any of these checks. The "read-only" / destructive / sed / path validations described by the module never run on actual command execution.

F2. validate_workspace_boundary is bypassed by the live tool dispatcher

file_ops::validate_workspace_boundary exists with #[allow(dead_code)] and is only called from read_file_in_workspace, write_file_in_workspace, edit_file_in_workspace. The tool dispatcher (tools::execute_tool_with_enforcer, lib.rs:1206-) routes the read_file / write_file / edit_file JSON tools to run_read_file, run_write_file, run_edit_file, which call the bare read_file / write_file / edit_file (tools/src/lib.rs:2071-2091). Those bare functions do not do any workspace boundary check.

Repro (illustrative; assumes the agent is invoked in workspace-write mode and has a write tool requirement of workspace-write):

{ "name": "write_file", "input": { "path": "/etc/test-claw-out", "content": "x" } }

policy.authorize("write_file", _) returns Allow when active_mode >= WorkspaceWrite. Then run_write_file -> write_file("/etc/test-claw-out", "x") -> fs::write runs against the absolute path. Nothing in the path goes through validate_workspace_boundary.

F3. is_within_workspace is a string-prefix check (also unused)

permission_enforcer::is_within_workspace (permission_enforcer.rs:177-191) does normalized.starts_with("{workspace_root}/") without canonicalizing. /workspace/dir/../../../etc/passwd starts with /workspace/ and would pass, even though the real target is /etc/passwd. This is moot in practice because check_file_write itself is not called from the dispatcher (see F2), but if it were re-wired without canonicalization the prefix bypass would persist.

F4. classify_bash_permission ignores shell expansion and chains

tools/src/lib.rs:1852 resolves the bash permission level to WorkspaceWrite when the first token is in a small read-only allowlist (cat, head, ls, ...) and has_dangerous_paths does not flag any token. has_dangerous_paths only looks for tokens that start with /, ~/, or ../. Therefore a command like:

cat README.md; rm -rf $HOME

is classified as workspace-write-only (because $HOME is not a token starting with / or ~/). The shell then expands $HOME to the user's real home directory and the chained rm -rf runs against it. Equivalent variants:

  • cat README; rm -rf $(printf '/h' && printf 'ome/$USER')
  • cat README; rm -rf `pwd`/../..
  • cat README; rm -rf ~root (HOME not used; the heuristic only checks ~/)

The same issue affects sed in-place (-i), tee, awk -i, and any tool whose name is not in the read-only list but whose effect is masked by leading cat/echo.

F5 (low). cargo audit reports

Three reachable advisories on rustls-webpki 0.103.10 (RUSTSEC-2026-0098, RUSTSEC-2026-0099, RUSTSEC-2026-0104) and unmaintained warnings on bincode 1.3.3 (RUSTSEC-2025-0141) and yaml-rust 0.4.5 (RUSTSEC-2024-0320). Filing this as a separate dependency bump PR.

Suggested fixes

  • Wire the existing bash_validation module into bash::execute_bash (or remove it if it's superseded).
  • Make run_read_file / run_write_file / run_edit_file go through the _in_workspace variants when a workspace root is set, and pass that root down from the dispatcher.
  • Replace is_within_workspace with a canonicalize-then-starts_with check on Path (not &str); reject targets whose canonical form escapes the workspace, including via symlinks.
  • Either restrict classify_bash_permission to commands without shell metacharacters / variable references, or run the full pipeline (chain split, expansion-aware path scan) before deciding mode.
  • Document the actual security boundary clearly. If the intent is "the agent can do whatever the LLM is told", the permission modes should not advertise scope they don't enforce.

Notes

  • This is a permission-model architecture issue, not a remote exploit. There is no untrusted-network attacker here. The risk is that the current API gives downstream operators (and the model) a false impression of how much the modes restrict.
  • Repository has no SECURITY.md and Private Vulnerability Reporting is disabled, so this is filed as a public issue. If you'd prefer to coordinate disclosure for any of the above, please enable PVR or add a SECURITY.md and ping me.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions