fix(tools): block search_files pattern flag injection in rg/grep - #50357
fix(tools): block search_files pattern flag injection in rg/grep#50357Vesna-9 wants to merge 1 commit into
Conversation
## What does this PR do?
The `search_files` content search built the ripgrep/grep command line by
shell-escaping the model-supplied `pattern` and appending it as a positional
argument with no end-of-options separator. Single-quoting via
`_escape_shell_arg` blocks *shell* injection, but the escaped token is still
handed to rg/grep as one argv element, which the tool happily parses as a
*flag* when it starts with a dash.
Before: a single `search_files` call with `pattern="--pre=/path/to/script"`
reached `_search_with_rg` as `rg ... '--pre=/path/to/script' <path>`. ripgrep
treats `--pre=CMD` as a preprocessor and runs CMD on every searched file, so
the pattern argument alone executes arbitrary commands on the host with the
agent's privileges. I confirmed this live: `rg --line-number --no-heading
--with-filename '--pre=/tmp/pre.sh' needle <tree>` ran the script. Because
`search_files` is in the default-enabled `file` toolset and is the agent's
primary code-search tool, a prompt-injected instruction in a page or file the
agent is asked to search can drive it. The grep fallback was injectable too
(e.g. `-f<file>` reads the pattern list from an arbitrary file).
After: a literal `--` end-of-options marker is inserted immediately before the
pattern in both `_search_with_rg` and `_search_with_grep`, so `--pre=...`,
`-f...`, and any other dash-prefixed token are treated as ordinary search
terms. A `--pre=value` pattern now matches that literal text and never starts
a preprocessor.
## Related Issue
N/A
## Type of Change
- [x] 🔒 Security fix
## Changes Made
- `tools/file_operations.py`: insert a `--` end-of-options token before the
escaped pattern/path in `_search_with_rg` (line ~2196) and
`_search_with_grep` (line ~2326); document why escaping alone is
insufficient.
- `tests/tools/test_search_error_guard.py`: add `TestSearchPatternFlagInjection`,
parametrized over both backends — one test drops a real executable
preprocessor and asserts a `--pre=<script>` pattern never runs it, another
asserts a dash-prefixed token is searched literally.
## How to Test
1. `scripts/run_tests.sh tests/tools/test_search_error_guard.py` — all pass.
2. Revert just the two `cmd_parts.append("--")` lines and rerun: the new
`TestSearchPatternFlagInjection` cases fail (rg reports
`preprocessor command could not start`, proving the flag was parsed).
3. Manual: `rg --line-number --no-heading --with-filename -- '--pre=/bin/sh'
<dir>` treats `--pre=/bin/sh` as a literal pattern instead of executing it.
## Checklist
### Code
- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits (`fix(scope):`, etc.)
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix (no unrelated commits)
- [x] I've run `pytest tests/ -q` and all tests pass
- [x] I've added tests for my changes (required for bug fixes)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)
### Documentation & Housekeeping
- [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A
- [x] I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
- [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
|
Duplicate of #44908 — same fix at the same site. Both insert a literal |
|
Thanks for the focused security fix. The premise remains verified on current main: The submitted diff applies cleanly to current main ( Automated hermes-sweeper review. |
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Three PRs address related search_files hardening at the rg/grep backend boundary, but their diffs target distinct causes: #39858 repairs masked search errors and diagnostic parsing, #40707 fixes the resulting files_only whitespace regression, and #50357 blocks option injection through dash-prefixed patterns.
Related pull requests
- #39858 [merged]
related— (+266/-18) — merged reference implementation: made rg/grep failures reachable with pipefail, separated diagnostics from usable payload, and preserved partial matches instead of parsing errors as results or discarding valid output. - #40707 [closed]
related— (+84/-3) — distinct regression fix, still relevant although closed: makes diagnostic splitting mode-aware so files_only preserves paths containing spaces while retaining hard-error handling. The diff addresses the keep_open review on #40707 by adding the requested parameterized end-to-end coverage for both production backends. - #50357
related— (+73/-4) — merge: inserts an end-of-options marker before the pattern in both rg and grep commands, preventing --pre/-f argument injection while preserving literal dash-prefixed searches; this matches the keep_open review on #50357 and includes real-backend regression tests.
Duplicates
None among #39858, #40707, and #50357: they modify the same search paths but fix different root causes. Contributor discussion identifies #44908 as the same-site, same-mechanism duplicate of #50357.
Suggested consolidation
Merge #50357 as the focused option-injection fix; its production hunks and backend tests directly cover the reported cause. Keep #39858 as the merged reference implementation and treat #40707 as a separate regression fix rather than a duplicate; no listed PR should be closed as a duplicate, while #44908 can be closed in favor of #50357 if its current diff remains mechanically identical.
Cross-PR triage: Reviewed 3 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 30 kB of PR diffs, 12 kB of issue/PR text, 4 kB of discussion (6 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
What does this PR do?
The
search_filescontent search built the ripgrep/grep command line byshell-escaping the model-supplied
patternand appending it as a positionalargument with no end-of-options separator. Single-quoting via
_escape_shell_argblocks shell injection, but the escaped token is stillhanded to rg/grep as one argv element, which the tool happily parses as a
flag when it starts with a dash.
Before: a single
search_filescall withpattern="--pre=/path/to/script"reached
_search_with_rgasrg ... '--pre=/path/to/script' <path>. ripgreptreats
--pre=CMDas a preprocessor and runs CMD on every searched file, sothe pattern argument alone executes arbitrary commands on the host with the
agent's privileges. I confirmed this live:
rg --line-number --no-heading --with-filename '--pre=/tmp/pre.sh' needle <tree>ran the script. Becausesearch_filesis in the default-enabledfiletoolset and is the agent'sprimary code-search tool, a prompt-injected instruction in a page or file the
agent is asked to search can drive it. The grep fallback was injectable too
(e.g.
-f<file>reads the pattern list from an arbitrary file).After: a literal
--end-of-options marker is inserted immediately before thepattern in both
_search_with_rgand_search_with_grep, so--pre=...,-f..., and any other dash-prefixed token are treated as ordinary searchterms. A
--pre=valuepattern now matches that literal text and never startsa preprocessor.
Related Issue
N/A
Type of Change
Changes Made
tools/file_operations.py: insert a--end-of-options token before theescaped pattern/path in
_search_with_rg(line ~2196) and_search_with_grep(line ~2326); document why escaping alone isinsufficient.
tests/tools/test_search_error_guard.py: addTestSearchPatternFlagInjection,parametrized over both backends — one test drops a real executable
preprocessor and asserts a
--pre=<script>pattern never runs it, anotherasserts a dash-prefixed token is searched literally.
How to Test
scripts/run_tests.sh tests/tools/test_search_error_guard.py— all pass.cmd_parts.append("--")lines and rerun: the newTestSearchPatternFlagInjectioncases fail (rg reportspreprocessor command could not start, proving the flag was parsed).Checklist
Code
fix(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A