Skip to content

Refuse ANSI-C and locale quoting in the Bash write guard (#575) - #577

Merged
Kewton merged 1 commit into
developfrom
feature/issue-575-security-tools-bash-ansi-c-program-option-566
Oct 3, 2026
Merged

Kewton merged 1 commit into
developfrom
feature/issue-575-security-tools-bash-ansi-c-program-option-566

Conversation

@Kewton

@Kewton Kewton commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Closes #575

What changed

  • src/tools/bash_write_guard/ansi_c_quoting.rs (new): scans the whole command for ANSI-C quoting $'…' and locale quoting $"…" outside quotes, in any word position.
    • Quote state follows shell_tokens: inside '…', \ is literal; inside "…" and outside quotes, \ escapes the next character.
    • "$'x'", '$' and \$'x' stay allowed, because bash does not expand them.
  • src/tools/bash_write_guard.rs: runs the scan at the start of write_targets.
    • The scan runs before shell_tokens, so the $'\'' misreading cannot hide the commands after it. That misreading makes shell_tokens return None, which leaves no write targets.
    • confinement_rejection rejects the result with the new operation ANSI-C / locale quoting.
    • The reason suggests alternatives such as printf 'a\tb\n' and grep -P '\t'.
  • tests/issue575_bash_ansi_c_quoting.rs (new): integration tests through path_confinement_rejection.

Why reject instead of decoding

  • Escape tables differ between shell versions.
  • The value of $"…" depends on the locale's translations.
  • Rejecting is honest failure and needs no decoder in each of the three lexers.
  • Recorded agent Bash commands use $' or $" 0 times.

Verification

  • Runner verify: 15/15 pass on 1c01ed71 (verify run 1176).
  • Before/after: on the earlier code, the new rejection table and the operation-name test fail, and the allow table passes. With this change, all pass.
  • cargo mutants (-- ansi_c_quoting, no --lib filter, --jobs 2), 19 mutants: 15 caught, 2 missed, 2 unviable.
    • One missed mutant is caught when measured again with --jobs 1.
    • The other is a pre-existing line in write_targets. Integration tests catch it.
    • No survivor remains in the new code.
  • Merge-gate review (Claude_Sub): 0 blockers.
    • The tables from the investigation were run again: no form changed from reject to allow.
    • These forms are all rejected: $'\'' misreading, cat $'.env', cat $'sub/link/secret', "a"$'tee', \\$'tee', $(…), backticks, <(…), heredoc bodies.
  • Unchanged and still passing: issue566_*, issue567_*, issue428_*, bash_workspace_confinement.

Notes

Developed with the CommandMate parallel-dev harness (PM: Claude Code, leader: Claude_Dev, workers: Command Code).

🤖 Generated with Claude Code

Issue #575. The write-guard lexer did not know ANSI-C (`$'...'`) or locale
(`$"..."`) quoting: it kept the `$` as an ordinary character and stripped the
following quote as an ordinary quote, so `$'tee'` read as the word `$tee`.
Bash expands those forms before running, so the program, its prefixes, and its
options were read wrong and a write could leave the workspace while
bash:verify auto-approval and secret detection were bypassed.

New leaf module `bash_write_guard/ansi_c_quoting.rs` scans the whole command
before the lexer and returns the first `$'` / `$"` outside a shell quote; quote
state mirrors `shell_tokens` (outside / `'...'` / `"..."` / `\` escape), so the
introducer inside a quote, after a backslash, or a lone `$` stays allowed. The
refusal reuses the env -S shape: a new operation value
"ANSI-C / locale quoting" is pushed to write_targets and rejected by
confinement_rejection, so the write is recognized (has_recognized_mutation is
true) and bash:verify auto-approval no longer applies. Event names/schema and
the other lexical analyses are unchanged; `$'tee'`-style spellings are refused
as unverifiable rather than expanded.

Two-point measurement (report)
- Before, develop@1e193ae7 with the new test and unmodified source:
  `cargo test --test issue575_bash_ansi_c_quoting` FAILED --
  `ansi_c_quoting_rejects_program_and_option_spellings` ("expected rejection:
  $'tee' /tmp/f") and `ansi_c_quoting_rejection_names_the_ansi_c_operation`
  ("expected rejection"). The allow test passed.
- After: 5 lib unit tests + 3 integration tests pass.

Mutation testing (report; cargo-mutants 27.1.0, test filter `ansi_c_quoting`
so both the lib unit tests and the integration tests run)
- New module:
  `cargo mutants -f src/tools/bash_write_guard/ansi_c_quoting.rs --jobs 2 --timeout 120 -o <out>/mutants-module2 -- ansi_c_quoting`
  => 13 mutants tested, 13 caught, 0 missed.
- New wiring lines only:
  `cargo mutants -f src/tools/bash_write_guard.rs -F 'bash_write_guard\.rs:(33|130):' ...`
  => 3 mutants: 2 caught (the confinement_rejection operation check and the
  write_targets early return), 1 unviable (the `vec![Default::default()]`
  replacement does not compile), 0 missed.
- Supplementary function-scope run (`-F 'write_targets|confinement_rejection'`)
  leaves 6 survivors, all on pre-existing lines this issue neither adds nor
  changes: the `/dev/null` skip (63/64), the `cd -` check (68), and the segment
  split (140). They survive because the second-layer `path_tokens` scan still
  rejects an outside path when the `/dev/null` skip is dropped, because the same
  assertions cover the changed conditions, and because the changed segment split
  yields an empty segment list that the fixtures do not distinguish. No survivor
  is on a line added by this change.

Scope (report)
- Declared and actual: src/tools/bash_write_guard.rs,
  src/tools/bash_write_guard/ansi_c_quoting.rs,
  tests/issue575_bash_ansi_c_quoting.rs. No other file changed; the referenced
  existing integration tests (issue566/567/428, bash_workspace_confinement) are
  untouched and pass.

Gates (report; all exit 0)
- cargo fmt --all -- --check
- python3 scripts/validate_codex_skills.py --tracked-only
- ruff check --isolated --select E4,E7,E9,F,I --ignore E402 <python paths>
- shellcheck scripts/*.sh
- python3 -m pytest tests/test_codex_orchestrate.py -q
- python3 -m unittest discover -s workspace/management/scripts -p 'test_*.py'
- python3 tests/eval/test_acceptance_contract.py
- python3 tests/eval/test_completion_contract_snapshots.py
- python3 tests/eval/test_false_positive_regression.py
- RUSTFLAGS='-D warnings' cargo clippy --all-targets -- -D warnings
- RUSTFLAGS='-D warnings' cargo test --all-targets
- ruff check --isolated --select E4,E7,E9,F,I --ignore E402 scripts/nextjs_domain_oracle.py tests/test_nextjs_domain_oracle.py
- npm ci --ignore-scripts --include=dev --prefix tests/nextjs_domain && python3 -m pytest tests/test_nextjs_domain_oracle.py -q

Report-note: the issue asks for a "report"; scope.allow permits only the three
files above, so this report is carried in the commit message and no report file
was added.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
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.

1 participant