Skip to content

fix(security): SAST audit — shell injection, exit codes, lazy regex + telemetry default#965

Closed
celstnblacc wants to merge 7 commits intortk-ai:masterfrom
celstnblacc:fix/all-security
Closed

fix(security): SAST audit — shell injection, exit codes, lazy regex + telemetry default#965
celstnblacc wants to merge 7 commits intortk-ai:masterfrom
celstnblacc:fix/all-security

Conversation

@celstnblacc
Copy link
Copy Markdown

Summary

Full SAST audit of the celstnblacc/rtk fork. All findings are in code we own — no upstream changes involved.

Critical / High

  • C-1 — Shell injection via sh -c: run_err / run_test in runner.rs passed the command string directly to Command::new("sh").args(["-c", command]). A semicolon in a test command name (e.g. make test; rm -rf /) would spawn extra shell processes. Fixed with shell_words::split() + Command::new(bin).args(rest) — metacharacters are now literal arguments, not shell syntax.

  • C-2 — Exit code not propagated: run_err / run_test returned Ok(()) even when the child process exited non-zero, causing CI to silently pass on failure. Fixed with exit_code_from_output()std::process::exit(code). Signal-killed processes return 128 + signal per Unix convention.

High

  • H-1 — Regex::new() inside function: summary.rs::extract_number() called Regex::new() on every invocation. Migrated to lazy_static! with one static per pattern (RE_PASSED, RE_FAILED, RE_SKIPPED, RE_IGNORED).

  • H-2 — Divide-by-zero panic: cc_economics.rs computed savings_blended by multiplying rtk_saved_tokens by totals.blended_cpt.unwrap() before blended_cpt was set. Reordered to derive blended_cpt = cc_cost / cc_total_tokens first, then assign both fields.

  • H-3 — Infallible unwrap() on stash subcommand: git.rs matched Some("pop") | Some("apply") | ... then immediately called subcommand.unwrap(). Replaced with Some(sub @ ("pop" | "apply" | "drop" | "push")) binding pattern — no unwrap needed.

Medium

  • M-1 — Telemetry on by default: TelemetryConfig::default() set enabled: true. This fork has stripped telemetry; the default now reflects that (enabled: false).

  • M-2 — CWD fallback produces empty path: resolved_command used unwrap_or_default() for the CWD fallback, which returns an empty PathBuf. Changed to unwrap_or_else(|_| PathBuf::from(".")).

  • M-3 — unwrap() on HashMap::get in report.rs: grouped.get(&base_cmd).unwrap() would panic if a key appeared in the iterator but not the map (possible under concurrent modification). Replaced with let Some(rules) = grouped.get(&base_cmd) else { continue }.

Low

  • L-2 — status.code().unwrap_or(1) throughout: All remaining ExitStatus::code().unwrap_or(1) calls in gh_cmd.rs, gt_cmd.rs, git.rs, and main.rs migrated to exit_code_from_status() — returns 128 + signal on signal termination instead of 1.

Test plan

  • cargo test --all — 1152 tests passing (5 ignored integration tests)
  • cargo clippy --all-targets — zero warnings
  • cargo build --release — clean build
  • New TDD tests added: test_split_command_semicolon_is_literal, test_split_exec_blocks_semicolon_injection, test_run_err_propagates_exit_code (#[ignore] — integration)
  • ShipGuard SAST scan — 0 findings on changed files

🤖 Generated with Claude Code

newblacc and others added 7 commits March 31, 2026 13:56
When RTK is invoked via Claude Code's PreToolUse hook, the hook keeps
its stdin pipe open for the duration of the session. rg/grep subprocesses
inherit this open pipe and block waiting for EOF — they never terminate.

On macOS with multiple concurrent grep calls this causes memory to
accumulate unboundedly, eventually triggering a kernel panic
(observed: ~514GB RAM+swap consumed, single process at ~198GB).

Fix: redirect subprocess stdin to /dev/null so children are decoupled
from the hook pipe and terminate normally after completing their search.

Fixes rtk-ai#897

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… to worktree root (rtk-ai#892)

fix rtk-ai#886 — permission bypass: RTK was ignoring Claude Code's permissions.allow
list and defaulting to Allow for any command not explicitly denied. Now loads
the allow list from settings.json alongside deny/ask rules. When an allow list
is present, commands not matching any allow pattern return Ask instead of Allow,
matching Claude Code's documented whitelist-by-default behaviour. Fully
backward-compatible: no allow list configured → existing Allow behaviour unchanged.

fix rtk-ai#892 — worktree RTK.md path: rtk init --codex in local (non-global) mode was
writing AGENTS.md and RTK.md as bare relative paths (PathBuf::from("RTK.md")),
anchored to the CWD at init time. When Codex runs from a git worktree the files
are not present and sed errors out. Fix: resolve paths via git rev-parse
--show-toplevel (worktree-aware) so files are always placed at the worktree root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P0 — resolved_command() now sets stdin to Stdio::null() by default.
This fixes the rtk-ai#897 stdin leak across ALL 128+ subprocess call sites,
not just grep_cmd.rs. Commands needing stdin can override explicitly.

P1 — Removed telemetry entirely. Deleted telemetry.rs (339 lines),
removed ureq/hostname/getrandom deps, cleaned dead code in config.rs,
tracking.rs, init.rs. Local tracking (rtk gain) is preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace eval with bash -c in benchmark.sh (SHELL-001 CRITICAL x 3)
- Add changelog entry for fork security patches (v0.34.2-security.1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…telemetry default

C-1: replace sh -c exec with shell_words::split + Command::new(bin).args(rest) in runner.rs
C-2: propagate child exit codes via exit_code_from_output/exit_code_from_status throughout
H-1: compile summary.rs regexes once at startup via lazy_static!
H-2: fix divide-by-zero panic in cc_economics savings_blended calculation
H-3: replace infallible stash subcommand unwrap with Some(sub @ (...)) pattern
M-1: TelemetryConfig::default() now sets enabled: false (no opt-in required)
M-2: CWD fallback in resolved_command uses "." instead of empty path
M-3: remove grouped.get().unwrap() panic path in report.rs with let-else
L-2: migrate all status.code().unwrap_or(1) to exit_code_from_status across git/main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


newblacc seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@celstnblacc
Copy link
Copy Markdown
Author

Closing — this is a fork-internal security audit for celstnblacc/rtk, not intended for upstream.

@celstnblacc celstnblacc closed this Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants