Skip to content

#613 [security][tools] Bash の閉じ込めの検査で、引用していない CR と / を含まない相対の語の symlink を確かめる - #636

Merged
Kewton merged 3 commits into
developfrom
feature/issue-613-security-tools-bash-cr-symlink-602-603
Oct 9, 2026
Merged

Kewton merged 3 commits into
developfrom
feature/issue-613-security-tools-bash-cr-symlink-602-603

Conversation

@Kewton

@Kewton Kewton commented Oct 9, 2026

Copy link
Copy Markdown
Owner

概要

Bash の閉じ込めの検査に、既存の見逃しが 2 種類ありました(#602・#603 の merge 前レビューで発見)。

  1. 引用していない CR:bash は CR で語を分けませんが、読み取り側と書き込み側の字句解析は CR を区切りとして扱っていました。外向き symlink の名前に CR が入っていると、検査は別の語を確かめ、外を読み書きできました(例:echo x > sub/cr⏎link/f、cat lfcr⏎+CRLF の行末)。
  2. / を含まない相対の語:静的な path の判定が / を必須にしていたので、その語自体が外を指す symlink だと外を読めました(例:cat lf、dd if=lf、source lf)。

この PR は、Issue 本文の設計(着手前調査 H-09 で 1 つに絞ったもの)どおりに直します。

変更(対象 8 ファイル、+598/−27)

  • 設計 1(CR を語の文字とする読み方を足し、今の読み方と両方で判定する)
    • src/tools/bash/path_tokens.rs:CrMode(Separator=今の読み方、WordChar=bash と同じ)。read_words が読み方を受け取る。
    • src/tools/bash/read_guard.rs:引用の外に CR があるときだけ 2 つ目の読み方を走らせ、候補を合わせる(確かめられない語はどちらかで見つかれば拒否)。
    • src/tools/bash_write_guard.rs:lex_shell に読み方を足し、書き込み先(write_targets・raw_write_targets)を 2 つの読み方で集めて合わせる。has_recognized_mutation・protected_path_mutation・confinement_rejection はこの和から求めるので、どれも今より緩まない。
    • src/tools/bash_write_guard/working_directory.rs:作業ディレクトリの候補を 2 つの読み方で求め、Inspection::merge(候補・行き先・/ のない語の和、undecidable の OR)で合わせる。
    • CRLF の行末は 1 つの改行に直さず、bash と同じく CR を直前の語に残す(名前の最後が CR の外向き symlink を見逃さない)。
  • 設計 2(/ を含まない静的な語は、symlink のときだけ確かめる)
    • src/tools/bash/read_guard.rs:/ のない静的な語と = の右側を集め、ワークスペースの直下と各作業ディレクトリの候補につないで lstat する。symlink(または NotFound・ENOTDIR 以外の lstat の失敗)のときだけ、今の literal の確かめ方で確かめる。
    • src/tools/bash.rs:その呼び出し 1 か所と、CrMode を共有するための pub(crate) mod path_tokens。
  • テスト:decision-cases.jsonl に 52 行(51 → 103 件。問題の 2 つの表のすべての形、「許可のまま」の表、true⏎rm 保護 path=0 1 0 1 0 0 と true⏎tee 外の絶対パス=1 1 1 0 0 0 の固定)。tests/issue582_bash_read_candidates.rs・tests/issue567_bash_glob_write_targets.rs と単体テストに、CR を含む symlink と直下の外向き symlink の fixture を足した。既存のテストの行は 1 行も消していない。fixture はテストの一時ディレクトリ(target の下)の中だけ。
  • 変えていないもの:path_guard.rs・shell_lexical.rs・sensitive_path.rs・hidden_path.rs・allow_policy.rs・planner/verify.rs・runner.rs・loop_run.rs・generality_guardrails.rs。event 名・schema も変えていない。

検証

  • commandmate verify の 15 ゲート:develop e6e00a4d を取り込んだ HEAD で 15/15 pass(d2304485、scope 8 本、違反 0)。
  • TMPDIR=/tmp cargo test --test issue582_bash_read_candidates --test issue567_bash_glob_write_targets(CI と同じ条件を作るため):取り込んだ HEAD で exit 0、19 passed・0 failed、28 passed・0 failed(issue567・issue582 の順)。
  • 二点測定(develop 0ccc81bd の src + この PR のテストと jsonl):新しい 5 本だけが FAIL し、既存のテストはすべて pass(echo x > sub/cr\rlink/f・cat lf・300 字の名前・判定の表の cr-redirect-write などが拒否されない)。
  • 変異テスト(cargo mutants --jobs 1、--in-diff、--lib と結合テスト 2 本):
    • 1 回目(70 件):caught 55・missed 5・unviable 10。許可が増える向きの missed 4 件(Inspection::merge の 3 件、joined_is_symlink の lstat の失敗の経路)は、テストを足して直した。
    • 直した後(対象の関数に絞って 8 件):caught 8・missed 0。
    • 残る 1 件(lex_shell の ==→!=)は 2 つの読み方が入れ替わるだけで和が同じ(同値の変異)。
  • merge 前レビュー(H-10):merge 可、blocker 0。HEAD は base と比べて 1193 件で緩む向きの変化が 0 件(R・A・W・P・S が 1→0、V が 0→1 になった行はない)。値が変わった 135 件はすべて R か W が 0→1 の厳しい向き。問題の 2 つの表は全部の形で拒否、「許可のまま」の表は値が変わらない。

この PR では直していない既存の穴(#613 の前からあり、この PR で緩んではいない)

誤拒否

  • 記録 436 件で CR を含むものは 0 件。/ のない語が拒否されるのは、その名前がワークスペースの中の外向き symlink のときだけ(記録の作業用ワークスペースには 0 個)。外向き symlink と同じ名前のコマンド(例:node)を使うときの拒否は、閉じ込めの約束として受け入れる。

Refs #613

🤖 Generated with Claude Code

Kewton and others added 3 commits October 8, 2026 21:28
Issue #613: the Bash read and write guards split words on a carriage
return, which Bash keeps inside the word, and the static path check
required a `/`. An outward symlink named `sub/cr\rlink`, `cr\rdir`,
`lfcr\r`, `lf`, `space lf`, or `wide lf` therefore escaped the read
confinement (and `cr\rdir`/`sub/cr\rlink` the write confinement).

- path_tokens: add `CrMode` and read a carriage return as an ordinary word
  character; `read_words` takes the reading.
- bash_write_guard / working_directory: `lex_shell` / `shell_tokens` take
  the reading; `write_targets`, `raw_write_targets`, and `inspect` union
  both readings, so a refusal or a recognized mutation is kept when either
  reading sees it.
- read_guard: union both readings, collect the `/`-less static read words
  (and the right-hand side of a non-assignment `NAME=value`), and prove
  them against the workspace root and each working-directory candidate,
  running the literal proof only when the joined path is an existing
  symlink. bash.rs calls it.
- fixtures/corpus: add the CR and `/`-less symlinks to both fixtures and
  the decision rows for the two problem tables, the allowed table, and the
  two pinned `true⏎…` values.

判断: design 2's "an undeterminable working directory refuses every
`/`-less word" is not applied, because
`tests/issue568_bash_cwd_write_targets.rs` (outside this issue's scope)
requires a bare `cd` chain that names no path to stay allowed; the
existing relative-candidate branch already refuses the words a `cd` could
redirect.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Issue #613 follow-up: cargo mutants found four survivors where dropping a
reading, an undecidable mark, or a proof route left the tests green.

- working_directory: `merge` unit test (bases union, targets,
  relative_words, undecidable OR, and no duplicated base) and an
  `inspect` test for the WordChar-only CR `cd` candidate. Kills the
  emptied `merge`, the missing `!` in the base-dedup check, and the
  `|=` -> `&=` undecidable mark.
- read_guard: `joined_is_symlink` unit test (an `ENAMETOOLONG` name needs
  the proof, a missing name does not) and a command-level
  over-long-name rejection. Kills the `!= NotFound` -> `== NotFound`
  narrowing.
- fixtures/corpus: an inside `cr␍sub` directory holding a nested outward
  symlink, plus the `cd cr␍sub && cat esc` row; only the merged WordChar
  candidate reaches it.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…issue-613-security-tools-bash-cr-symlink-602-603
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