Repository navigation
[security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568
Description
背景
#509(bash_write_guard の変異テスト)の着手前調査(2026-09-30、develop@beb0fdc)で見つけた見逃しです。2026-10-03 の着手前調査(develop@3581d77、#566・#575 の merge 後)で、範囲と直し方を確かめました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中で echo・pwd を使って確かめただけです。
Bash の閉じ込めの判定は 2 段です。1 段目は bash_write_guard の書き込み先の抽出、2 段目は path の候補の検査です。
問題(背景)
相対の書き込み先を、常にワークスペースの root を基準に判定し、同じコマンドの中の cd の後の cwd を反映しません。pushd・popd は認識しません。下の形はすべて許可されます(sub/link・linked-outside は外を指す symlink、root の link はない)。
| 分類 | 入力(例) |
|---|---|
| Issue の形 | cd sub && tee link/f、cd sub && printf x > link/f、pushd linked-outside && printf x > f |
| 区切りによらない | cd sub; tee link/f、改行、||、|、&、(cd sub); tee link/f、{ cd sub; tee link/f; }、if cd sub; then tee link/f; fi |
| cd の綴りと重ね | cd -P sub、cd -- sub、cd "sub"、cd $ROOT/sub(root の中の絶対 path)、cd sub && cd link && touch f(2 つ目の cd の先を root 基準で判定する) |
| CDPATH | CDPATH=sub cd link && tee f、export CDPATH=sub; cd link; tee f(字面に / がなくても外へ出る) |
| 前置き付き | builtin cd sub && tee link/f、command cd sub && … |
| 読み取り(2 段目) | cd sub && cat link/secret、cd sub && grep -r x link(cat sub/link/secret は拒否される) |
| 保護された path | cd tests && cp a.txt spec.rs は保護された tests/spec.rs に当たるのに検出されない |
影響(背景)
symlink の先のどこへでも書き込め、読み取れます。保護された path の検出も見逃します。bash:verify の自動許可には影響しません(書き込みありの判定は今も真)。
決めた設計(2026-10-03)
- cwd の候補の和集合で追い、決められない場合は拒否する(C 案)。候補はワークスペースの root からの相対 path の列で、始まりは root だけ。cd・pushd で先が決められるとき、今の候補のそれぞれに先を足した path を加える(置き換えない)。subshell・pipeline・
||で cwd が戻るかどうかを真似る必要がなく、root の候補が残るので抜けない。- 決められる先:字面のまま(
$・バッククオート・~・glob を含まない)、-でない、..を含まない。絶対 path は root からの相対に直す。 - cd・pushd の先そのもの("working directory" の対象)も、それまでの候補のそれぞれを基準に判定する(
cd sub && cd linkを拒否する)。 - popd、pushd(引数なし)、pushd ±N は候補を加えない。pushd -n DIR は DIR を加える。
- 決められる先:字面のまま(
- 相対の書き込み先(絶対・
~・$で始まるもの・-を除く)は、root に加えてすべての候補を基準に判定する。どれか 1 つでも外なら拒否する。 - 決められない場合:語に CDPATH が出る、または cd・pushd・popd があり、同時にループ(for・while・until・select)か関数の定義(function・name())がある場合。このとき相対の書き込み先が 1 つでもあれば拒否する(正直な失敗)。cd がない場合と、書き込み先がない場合は今のまま。判定に使う語は、生の文字列ではなく shell_tokens の token から読む(コメントの中の語で誤拒否しないため)。
- 実行時の CDPATH を外す:Bash の sh -c を起こす所で、環境の CDPATH を取り除く。静的な判定では環境の CDPATH を知ることができないため。
- pushd は cd と同じく "working directory" として記録し、前置きの走査で書き込みの program として扱う(pushd・popd)。
- 2 段目(読み取り)も同じ候補で判定する:path の候補の検査で、相対の候補を root に加えてすべての候補を基準に確かめる。決められない場合に相対の候補があれば拒否する。[security][tools] Bash の 2 段目の読み取りの判定が、引用符のない glob・動的な値・glob を含む絶対パスで抜け、ワークスペース外を読める(#567 の調査で発見) #571(glob・動的な値の読み取り)とは別の形なので、[security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568 で扱う。
- 保護された path の検出も同じ候補で照合する:
cd tests && cp a.txt spec.rsを検出し、cd tests && touch other.rsは当たらない。 - 候補は WriteTarget に足さず、別の構造として持つ。write_targets の出力、書き込みありの判定、bash:verify の自動許可は変えない(
cd frontend && npm testは自動許可のまま)。決められない場合の拒否も confinement_rejection の中だけで作る。拒否の operation は既存の値("working directory" か書き込みの種類)を使い、event の名前と schema は変えない。 - 字句解析(shell_tokens・ShellToken)と前置きの Resolution は変えない。候補の計算は token の列を受け取る別の関数(新しい葉の module)にする。write_targets の最初の数行(ANSI-C 引用の走査の早い return と shell_tokens の呼び出し)は変えない。shell_tokens が None の場合は今のまま([security][tools] Bash の書き込み検査が、字句解析で読めないコマンド(heredoc・コメントの中の ')を丸ごと許可し、ワークスペース外へ書き込める(#575 の調査で発見) #576 の範囲)。
- exec の前置き(env・nohup・timeout・sudo など)の後の cd は、shell の cwd を変えない。C 案では root の候補が残るので区別しなくても緩まない。今と同じく "working directory" の対象として判定する(厳しい側)。[security][tools] Bash の書き込み検査が env・sudo・timeout などの前置きや
{ … }の中の書き込みを認識せず、ワークスペース外へ書き込める(#509 の調査で発見) #566 の設計 5 の区別は入れない。
範囲外(develop にもとからある、#502 の類):X=cd; $X sub、eval "cd sub"、source・alias、cd sub && sh -c '…'、同じコマンドの中で symlink を作る・名前を変える形。
対象ファイル
src/tools/bash_write_guard.rs— 候補の配線(confinement_rejection・protected_path_mutation)、pushd の記録、test modulesrc/tools/bash_write_guard/working_directory.rs— 新規。cwd の候補の計算、決められない場合の判定、単体テストsrc/tools/bash_write_guard/command_prefix.rs— pushd・popd を書き込みの program の一覧に足す(数行)src/tools/bash.rs— 2 段目の相対の候補の判定と、sh -c の環境から CDPATH を外す(数行)tests/issue568_bash_cwd_write_targets.rs— 新規。path_confinement_rejection を呼ぶ結合テスト
受け入れ条件
- 下の「拒否する形」がすべて拒否され、「許可のままの形」が許可のままである
-
cd tests && cp a.txt spec.rsが保護された path として検出され、cd tests && touch other.rsは検出されない - sh -c の環境に CDPATH が渡らないことをテストで固定した
- 直す前の develop で新しいテストが失敗し、直した後で通ることを report に記録した(二点測定)
- 新しい module と配線の行だけで cargo mutants を実行し(下の「作業の場所」のコマンド、上限 30 分)、生き残った変異に理由を添えて report に書いた
- 既存の結合テスト(下の「回帰で通す既存のテスト(参考)」)は変更せず、そのまま通る
- 書き込みありの判定と bash:verify の自動許可が変わらない(
cd frontend && npm test、cd crates/x && cargo testは自動許可のまま) - 既存の検証・受入・証跡・リリースゲートを緩めていない(AGENTS.md の honest-failure 方針)
- 成長上限(docs/dev/dev-guardrails.md)の baseline を引き上げていない
テストの設計(参考)
fixture:sub/link・linked-outside・a/b/link は外を指す symlink、root の link はない。root の elink は外、sub/elink は中。
拒否する形:
- 問題の表の形すべて(読み取りは判定の関数で確かめるだけで、実際には読まない)
cd sub | tee link/f、cd a; cd b; tee link/f、pushd sub && tee link/f、pushd -n sub && tee link/ffor i in 1; do cd sub; done; tee link/f、f() { cd sub; }; f; tee link/fenv cd sub && tee link/f、env cd sub && tee elink/fcd sub && $'tee' link/f(ANSI-C 引用の拒否が先に効く)
許可のままの形:
cd src && touch a、cd src && echo x > a.txt、cd frontend && npm test > out.log、cd crates/x && cargo test 2>&1 | tee test.log、cd $ROOT && mkdir -p out、cd sub && tee /dev/nullcd frontend && npm test、cd crates/x && cargo test(自動許可のまま)pushd sub、popd、dirs、cd link(書き込みなし)
テストの関数名には、すべて working_directory を含めます(変異テストの名前のフィルタで、単体テストと結合テストの両方が走るようにするため)。
回帰で通す既存のテスト(参考)
- tests/issue575_bash_ansi_c_quoting.rs
- tests/issue566_bash_command_prefix_write_targets.rs
- tests/issue567_bash_glob_write_targets.rs
- tests/issue428_bash_path_tokens.rs
- tests/bash_workspace_confinement.rs
作業の場所(参考)
- ビルドとテストは CARGO_TARGET_DIR=/Volumes/SSD_NX/tmp/issue-568-20261003-01/target-568 と CARGO_INCREMENTAL=0 で行います。
- 変異テストのコマンド(--lib は付けない。付けると結合テストが走らない):
cargo mutants -f src/tools/bash_write_guard/working_directory.rs --jobs 2 --timeout 120 -o /Volumes/SSD_NX/tmp/issue-568-20261003-01/mutants -- working_directory
配線の行は -F で足します。ベースラインで単体テストと結合テストの両方が走ることを確かめます。打ち切った後は、ソースを touch してビルドし直します。 - 着手前調査の report:ローカルの workspace/tmp/1003/issue-568/results/H-01/report.md。probe と試作の patch:/Volumes/SSD_NX/tmp/issue-568-20261003-01/probe。
関連(参考)
- [security][tools] Bash の書き込み検査が env・sudo・timeout などの前置きや
{ … }の中の書き込みを認識せず、ワークスペース外へ書き込める(#509 の調査で発見) #566(前置き。設計 5 の区別は C 案では不要)、[security][tools] Bash の書き込み検査の字句解析が ANSI-C 引用 $'…' を扱わず、program や option を見逃してワークスペース外へ書き込める(#566 の再確認で発見) #575(ANSI-C 引用)、[security][tools] Bash の書き込み検査が、字句解析で読めないコマンド(heredoc・コメントの中の ')を丸ごと許可し、ワークスペース外へ書き込める(#575 の調査で発見) #576(コメント・heredoc。同じ字句解析)、[security][tools] Bash の 2 段目の読み取りの判定が、引用符のない glob・動的な値・glob を含む絶対パスで抜け、ワークスペース外を読める(#567 の調査で発見) #571(読み取りの抜け道)、[security][tools] Bash の字句検査では、プログラム経由の間接書き込みや検証コマンドの副作用を閉じ込められない #502(字句検査の限界)
根拠(develop@3581d77)
- src/tools/bash_write_guard.rs:248(cd は "working directory" の対象を 1 つ記録するだけ)
- src/tools/bash_write_guard.rs:68(cd - だけ拒否)
- src/tools/bash.rs:785(2 段目の候補の loop。root 基準)
- src/tools/bash.rs:228-229(sh -c の起動)
Activity
Merge pull request #579 from Kewton/feature/issue-568-security-tools-…
Kewton commentedon Oct 3, 2026
PR #579 で develop に merge しました(66aa33230812fdc5382d45159d3022cc125063a0)。検証 15/15、merge 前レビューの blocker 2 件は直す回で直し、再確認で blocker 0。後続は #502(名前を組み立てる形、eval・trap など)へのコメントと #580(決められない状態の誤拒否)で扱います。