Skip to content

SECURITY [P1]: RIPGREP_CONFIG_PATH env var + rg --pre enables silent arbitrary-code-execution via every contextcrawler grep call #32

Description

@thehoff

Severity: P1 (RCE)

Confirmed end-to-end this evening (2026-05-18 06:46 UTC) by adversarial code review + independent empirical re-verification on the deployed contextcrawler 0.1.7 binary. Not a false alert.

What

ripgrep honors two env vars (RIPGREP_CONFIG_PATH, RIPGREP_CONFIG_FILE) that load a config file. That config file can contain --pre=<script> and --pre-glob=*, which makes rg execute <script> as a per-file preprocessor. Both contextcrawler grep paths (the canonical filter at src/cmds/system/grep_cmd.rs AND the new format-flag passthrough at src/main.rs:run_grep_format_passthrough) invoke rg as a subprocess without clearing these env vars.

A user whose shell env has RIPGREP_CONFIG_PATH set to an attacker-controlled file gets arbitrary code execution on their next contextcrawler grep ... call. No CLI arguments need to differ from normal usage — the attack is invisible to the user typing contextcrawler grep foo bar.

Attack surface that elevates severity in this codebase:

  • contextcrawler is invoked by hooks for both Claude Code (auto-wrap on every Bash tool call) and Codex (AGENTS.md MUST rule, ~80% adoption today). A tainted env → any agent invocation that touches grep → silent code execution.
  • The Tirith integration + the supply-chain audit posture (per CHANGELOG v0.1.6) set user expectations that contextcrawler defends against this class.
  • Vector requires only the ability to set ONE env var. Common compromise paths: malicious npm postinstall, supply-chain pip install, hostile dotfile drop, dirty CI runner.

Reproduction (verified end-to-end, 4 variants)

(Redacted to not seed a working PoC in a public issue. Full reproduction lives in the corresponding fix-PR's negative-regression test fixture under tests/security/.)

Summary of what was verified:

  1. Canonical grep path + env var alone (no CLI --pre) → preprocessor fired. Confirmed via PID/UID-logged file.
  2. New format-flag bypass path + env var alone → preprocessor fired.
  3. Canonical grep path with --pre in extra_args (no env var) → preprocessor fired.
  4. Control case: raw rg with env var (no contextcrawler) → preprocessor fired (confirms rg's design behavior; contextcrawler inherits it).

All four logged matching PID/PPID lineage as the user's uid.

Affected versions: contextcrawler 0.1.7 (deployed today) and likely all prior versions that called rg without env-clearing (this is a long-standing pre-existing exposure that was uncovered by today's security review, NOT a regression introduced by today's PRs).

Fix (proposed)

Two changes, both applied to both call sites that spawn rg (src/main.rs::run_grep_format_passthrough AND src/cmds/system/grep_cmd.rs::run):

  1. Clear the rg config env vars before spawning:
    Command::new(rg_path)
        .env_remove("RIPGREP_CONFIG_PATH")
        .env_remove("RIPGREP_CONFIG_FILE")
        .args(user_args)
        ...
  2. Reject --pre, --pre-glob, --search-zip in user args (both the canonical extra_args and the bypass's user_args) with a helpful error pointing at contextcrawler proxy rg ... for users with a legitimate need. These flags exist for power users; agents and hooks shouldn't be forwarding them.

Regression tests (must accompany the fix)

tests/security/grep_env_rce.rs (new):

  • Negative test: with RIPGREP_CONFIG_PATH pointing at a config that would fire a preprocessor, run contextcrawler grep and assert the preprocessor file marker does NOT appear.
  • Negative test: same for RIPGREP_CONFIG_FILE.
  • Negative test: passing --pre /tmp/... directly returns a rejection (exit non-zero with the documented error).
  • Positive test: normal grep invocations (no env, no --pre) still work.

These tests become the regression gate — any future change that re-enables env-var inheritance or --pre forwarding fails CI.

Out of scope (separate issues)

  • --search-zip reads zip archives; zip-slip is rg's own surface but worth a docs/notes mention. File as P3 follow-up.
  • PATH-based binary substitution of rg itself (which::which() trusts $PATH) — standard exposure, separate hardening ticket.
  • Resource-cap parity gap (the bypass uses inherited stdio + .status() vs canonical's exec_capture() with 64 MiB caps + stdin-null). Separate P2 follow-up.

References

  • Adversarial codex review output: /tmp/security-review-grep.txt (local, redacted before publishing)
  • Independent verification log: this session's terminal output 2026-05-18 06:43-06:46 UTC
  • Affected functions:
    • src/main.rs:1469-1535 (today's bypass)
    • src/cmds/system/grep_cmd.rs:43-48 (canonical, pre-existing)
    • src/core/utils.rs:resolved_command (rg path resolution — separate hardening ticket)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions