Skip to content

Shell-quote agent-controlled paths in AI tool helpers#11131

Open
tkshsbcue wants to merge 1 commit into
warpdotdev:masterfrom
tkshsbcue:fix/shell-quote-agent-path-helpers
Open

Shell-quote agent-controlled paths in AI tool helpers#11131
tkshsbcue wants to merge 1 commit into
warpdotdev:masterfrom
tkshsbcue:fix/shell-quote-agent-path-helpers

Conversation

@tkshsbcue
Copy link
Copy Markdown

@tkshsbcue tkshsbcue commented May 17, 2026

Shell-quote agent-controlled paths in AI tool helpers

Summary

Fixes #11132

Five sites in the AI agent's tool helpers (app/src/ai/blocklist/action_model/execute/) built shell commands by interpolating an agent-controlled path inside a double-quoted string:

format!("test -f \"{path}\"")                                  // execute.rs
format!("if (Test-Path -PathType Leaf \"{path}\") ...")        // execute.rs (PowerShell)
format!("git -C \"{absolute_path}\" rev-parse")                // execute.rs
format!(" \"{target_path}\"")                                  // grep.rs (appended to git grep / grep)
"Get-ChildItem -Path \"{}\" -Recurse -File | ..."              // grep.rs (Select-String)
format!("find \"{target_path}\" -type f {pattern_args}")       // file_glob.rs
format!("Get-ChildItem ... -Path \"{target_path}\" ...")       // file_glob.rs (PowerShell)

Inside POSIX "...", the shell still expands $VAR, `...`, and $(...). Inside PowerShell "...", the shell still expands $x (including $env:USERPROFILE) and treats ` specially. POSIX filesystems allow all of these bytes in a path component and PowerShell accepts them in -Path arguments, so a path that contains shell metacharacters gets re-interpreted by the shell when these helpers run.

is_file_path and is_git_repository are the sharpest case: they are called as side-effects of the grep and file_glob tools (run_grepis_file_path + is_git_repository, run_file_globis_git_repository), so they execute on the user's shell without the per-command approval gate that user-visible commands flow through. A path argument the agent copies out of prompt-injected content — a file it read, a fetched web page, an MCP response — that contains $(...) therefore runs that substitution silently.

The same fix template already exists in the repo: warp_util::path::ShellFamily::shell_escape covers both POSIX (backslash) and PowerShell (backtick), and From<ShellType> for ShellFamily makes the dispatch one line. This PR routes every unsafe path interpolation through it.

What changed

app/src/ai/blocklist/action_model/execute.rs

  • Extracted pure helpers build_is_file_path_command and build_is_git_repository_command so the emitted command string is unit-testable byte-for-byte. Both wrap the path with ShellFamily::from(shell_type).shell_escape(...).
  • is_file_path and is_git_repository are now thin async wrappers over those helpers.

app/src/ai/blocklist/action_model/execute/grep.rs

  • run_git_grep_command, run_grep_command, and run_select_string_command now escape target_path with the appropriate ShellFamily before appending it to the command.

app/src/ai/blocklist/action_model/execute/file_glob.rs

  • run_find_command and run_powershell_get_childitem_command likewise route target_path through ShellFamily::shell_escape.

app/src/ai/blocklist/action_model/execute_tests.rs

  • New path_shell_quoting module — 14 tests pinning byte-exact output of the two probe-command builders across Bash, Zsh, Fish, and PowerShell, with payloads covering plain paths, spaces, $(...), backticks, $HOME, $env:USERPROFILE, and ; chains.

Manual Testing

The change is exclusively to the byte sequence emitted by the helpers above. Those strings are handed to Session::execute_command, which on a local session is run via <shell> -c <command_string> (local_command_executor.rs:55-57), so the runtime effect is reproducible at the shell level without Warp.

Setup

mkdir -p "/tmp/innocent\$(touch ~/PWNED_BY_AGENT)" && rm -f ~/PWNED_BY_AGENT

Before this PR — what is_file_path used to emit

$ test -f "/tmp/innocent$(touch ~/PWNED_BY_AGENT)"
$ ls ~/PWNED_BY_AGENT
/Users/me/PWNED_BY_AGENT          # file was created by the probe

The $(touch ...) substitution runs before test -f, so the agent's silent existence check is enough to execute arbitrary commands as the user.

After this PR — what is_file_path emits now

$ test -f /tmp/innocent\$\(touch\ ~/PWNED_BY_AGENT\)
$ ls ~/PWNED_BY_AGENT
ls: /Users/me/PWNED_BY_AGENT: No such file or directory

$ and ( are backslash-escaped, so the substitution is treated as literal characters and test -f simply reports that the (admittedly oddly-named) path does not exist. The same demonstration applies to git -C ... rev-parse, git grep, grep, find, Test-Path, Select-String, and Get-ChildItem.

Automated Testing

  • cargo test -p warp --lib 'ai::blocklist::action_model::execute'75 passed, 0 failed (14 new tests in path_shell_quoting).
  • cargo clippy -p warp --lib --tests -- -D warnings — clean.

Reproduction (in Warp itself, before the fix)

  1. Have the agent read any text that contains an attacker-controlled path token, e.g. a file in the repo, a fetched web page, or an MCP tool response with body "check path /tmp/x$(touch ~/PWNED)".
  2. The agent calls the grep or file_glob tool with that path as the target.
  3. run_grep invokes is_file_path (silent, no approval gate), which emits test -f "/tmp/x$(touch ~/PWNED)" to the user's shell.
  4. The shell evaluates $(touch ~/PWNED) before test -f, creating ~/PWNED.

After this PR, step 3 emits test -f /tmp/x\$\(touch\ ~/PWNED\) and no substitution occurs.

Out of scope (follow-up)

The grep-query interpolations (escape_double_quotes / powershell_escape_double_quotes) only escape " and leave $ and backticks unescaped. They are not addressed here because queries are regex bodies rather than paths, but the same bug class applies and is a sensible follow-up.

Five sites in the AI agent's tool helpers built shell commands by
interpolating an agent-controlled path into a double-quoted string:

    format!("test -f \"{path}\"")
    format!("if (Test-Path -PathType Leaf \"{path}\") ...")
    format!("git -C \"{absolute_path}\" rev-parse")
    format!(" \"{target_path}\"")               // appended to git grep / grep
    format!("find \"{target_path}\" -type f ...")
    format!("Get-ChildItem ... -Path \"{target_path}\" ...")

Inside POSIX `"..."`, the shell still expands `$VAR`, `` `...` ``, and
`$(...)`. Inside PowerShell `"..."`, the shell expands `$x` (including
`$env:USERPROFILE`) and treats `` ` `` specially. POSIX filesystems
allow all of these bytes in a path component, and PowerShell accepts
them in `Path` arguments, so a path containing shell metacharacters
gets re-interpreted by the shell when these helpers run.

`is_file_path` and `is_git_repository` (in `execute.rs`) are the
sharpest case: they are called as side-effects of the `grep` and
`file_glob` tools (see `grep.rs:run_grep` and
`file_glob.rs:run_file_glob`), so they execute on the user's shell
*without* the per-command approval gate that user-visible commands go
through. A `path` argument the agent picked up from prompt-injected
content — a file it `read`, a fetched web page, an MCP response — that
contains `$(...)` therefore runs that substitution silently.

The repo already exposes the right primitive:
`warp_util::path::ShellFamily::shell_escape` (POSIX backslash-escape /
PowerShell backtick-escape), and `From<ShellType> for ShellFamily`
makes the dispatch one line. Route every unsafe path interpolation
through it:

* `app/src/ai/blocklist/action_model/execute.rs` — `is_file_path`,
  `is_git_repository`. Command construction extracted into pure
  `build_is_file_path_command` / `build_is_git_repository_command`
  so the byte-exact shape can be pinned by unit tests.
* `app/src/ai/blocklist/action_model/execute/grep.rs` — `target_path`
  in `run_git_grep_command`, `run_grep_command`, and
  `run_select_string_command`.
* `app/src/ai/blocklist/action_model/execute/file_glob.rs` —
  `target_path` in `run_find_command` and
  `run_powershell_get_childitem_command`.

`cargo test -p warp --lib 'ai::blocklist::action_model::execute'` →
75 passed (14 new tests cover plain paths, spaces, `$(...)`,
backticks, `$VAR`/`$env:USERPROFILE`, `;` chains, across Bash, Zsh,
Fish, and PowerShell).

`cargo clippy -p warp --lib --tests -- -D warnings` → clean.

The grep-*query* interpolations (`escape_double_quotes` /
`powershell_escape_double_quotes`) only escape `"` and leave `$` and
backticks unescaped. They are out of scope for this commit because
queries are regex bodies rather than paths, but the same bug class
applies and is a sensible follow-up.
@cla-bot cla-bot Bot added the cla-signed label May 17, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 17, 2026

@tkshsbcue

This PR is not linked to an issue that is marked with ready-to-implement.

Issue-state enforcement details:

Readiness check:

To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement.

Powered by Oz

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 17, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkshsbcue

This PR is not linked to an issue that is marked with ready-to-implement.

Issue-state enforcement details:

  • Associated same-repo issues checked: none

  • Required readiness label: ready-to-implement

To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement.

Powered by Oz

@tkshsbcue
Copy link
Copy Markdown
Author

/oz-review

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkshsbcue

This PR is not linked to an issue that is marked with ready-to-implement.

Issue-state enforcement details:

  • Associated same-repo issues checked: #11132

  • Required readiness label: ready-to-implement

Readiness check:

  • #11132: missing ready-to-implement; readiness labels present: none

To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement.

Powered by Oz

@tkshsbcue
Copy link
Copy Markdown
Author

/oz-review

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkshsbcue

This PR is not linked to an issue that is marked with ready-to-implement.

Issue-state enforcement details:

  • Associated same-repo issues checked: #11132

  • Required readiness label: ready-to-implement

Readiness check:

  • #11132: missing ready-to-implement; readiness labels present: none

To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement.

Powered by Oz

@tkshsbcue
Copy link
Copy Markdown
Author

/oz-review

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tkshsbcue

This PR is not linked to an issue that is marked with ready-to-implement.

Issue-state enforcement details:

  • Associated same-repo issues checked: #11132

  • Required readiness label: ready-to-implement

Readiness check:

  • #11132: missing ready-to-implement; readiness labels present: none

To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement.

Powered by Oz

@tkshsbcue
Copy link
Copy Markdown
Author

Could a maintainer take a triage pass so ready-to-implement can land and Oz unblocks the PR? Happy to revise based on review feedback. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AI agent path helpers emit unquoted paths to test -f / git -C / grep / find, breaking paths with shell metacharacters

1 participant