Shell-quote agent-controlled paths in AI tool helpers#11131
Conversation
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.
|
This PR is not linked to an issue that is marked with Issue-state enforcement details:
Readiness check:
To continue, link this PR to a same-repo issue such as Powered by Oz |
There was a problem hiding this comment.
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
|
/oz-review |
There was a problem hiding this comment.
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
|
/oz-review |
There was a problem hiding this comment.
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
|
/oz-review |
There was a problem hiding this comment.
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
|
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! |
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: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-Patharguments, so a path that contains shell metacharacters gets re-interpreted by the shell when these helpers run.is_file_pathandis_git_repositoryare the sharpest case: they are called as side-effects of thegrepandfile_globtools (run_grep→is_file_path+is_git_repository,run_file_glob→is_git_repository), so they execute on the user's shell without the per-command approval gate that user-visible commands flow through. Apathargument the agent copies out of prompt-injected content — a file itread, 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_escapecovers both POSIX (backslash) and PowerShell (backtick), andFrom<ShellType> for ShellFamilymakes the dispatch one line. This PR routes every unsafe path interpolation through it.What changed
app/src/ai/blocklist/action_model/execute.rsbuild_is_file_path_commandandbuild_is_git_repository_commandso the emitted command string is unit-testable byte-for-byte. Both wrap the path withShellFamily::from(shell_type).shell_escape(...).is_file_pathandis_git_repositoryare now thin async wrappers over those helpers.app/src/ai/blocklist/action_model/execute/grep.rsrun_git_grep_command,run_grep_command, andrun_select_string_commandnow escapetarget_pathwith the appropriateShellFamilybefore appending it to the command.app/src/ai/blocklist/action_model/execute/file_glob.rsrun_find_commandandrun_powershell_get_childitem_commandlikewise routetarget_paththroughShellFamily::shell_escape.app/src/ai/blocklist/action_model/execute_tests.rspath_shell_quotingmodule — 14 tests pinning byte-exact output of the two probe-command builders acrossBash,Zsh,Fish, andPowerShell, 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
Before this PR — what
is_file_pathused to emitThe
$(touch ...)substitution runs beforetest -f, so the agent's silent existence check is enough to execute arbitrary commands as the user.After this PR — what
is_file_pathemits now$and(are backslash-escaped, so the substitution is treated as literal characters andtest -fsimply reports that the (admittedly oddly-named) path does not exist. The same demonstration applies togit -C ... rev-parse,git grep,grep,find,Test-Path,Select-String, andGet-ChildItem.Automated Testing
cargo test -p warp --lib 'ai::blocklist::action_model::execute'— 75 passed, 0 failed (14 new tests inpath_shell_quoting).cargo clippy -p warp --lib --tests -- -D warnings— clean.Reproduction (in Warp itself, before the fix)
"check path /tmp/x$(touch ~/PWNED)".greporfile_globtool with that path as the target.run_grepinvokesis_file_path(silent, no approval gate), which emitstest -f "/tmp/x$(touch ~/PWNED)"to the user's shell.$(touch ~/PWNED)beforetest -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.