背景
#509 (bash_write_guard の変異テスト)の着手前調査(2026-09-30)で見つけた見逃しです。2026-10-01 の着手前調査(develop@ad9ff41 )で、原因と直し方を確かめ、Issue に書いていなかった形も見つけました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中の /bin/sh(bash 3.2、POSIX モード)でだけ確かめています。
問題(背景)
書き込み先の glob(* ? [)と brace({a,b}・{a..z})を、展開前の文字列のまま判定します。展開前の名前は存在しないので、未作成の扱いで親(ワークスペースの root)まで戻り、内側と判定されます。実行時の展開で、中の symlink(外を指す)や親ディレクトリ(..)に届きます。
入力(例)。linked-outside は外を指す symlink
今の判定
届く先
tee lin*/secret、cp a.txt lin*/、chmod 777 lin*/secret
許可
symlink の先の既存のもの(glob は既存のものにだけ一致する)
tee {linked-outside,x}/f、tee {k..m}inked-outside/f、tee {linked-outside/f,x}
許可
symlink の先(brace は存在に関係なく展開される)
tee .*/f、tee .?/f、tee {.,}./f、tee {.,.}./f
許可
親ディレクトリ(.. になる)
影響(背景)
/tmp に限らず、symlink の先や親ディレクトリへ書き込めます。
着手前調査の結果(背景。2026-10-01)
書き込み先の検査(path_guard の ensure_bash_write_target)は、Bash の 2 段目の読み取りのパスの判定(bash.rs の bash_path_allowed)でも使われます。そのため「glob・brace の文字を含むものを一律に拒否する」と、引用符の中の Next.js の動的ルート("src/app/[id]/page.tsx")の読み取り・書き込みや、cat src/{main,lib}.rs を誤って拒否します(試作で既存のテスト 4 件が失敗)。
書き込み先の文字列は引用符を外した後の値なので、'src/app/[id]' と src/app/[id] は区別できません。
引用符のない相対の glob の読み取り(cat lin*/secret など)にも抜け道がありますが、別の Issue で扱います(この Issue の範囲外)。
決めた設計(2026-10-01)
展開して、一致したものをすべて今の証明で確かめる。 raw が glob・brace の文字を含まなければ今のとおり(変更なし)。
brace を先に文字列として展開する :入れ子、,、連番({a..z}・{1..9})、/ をまたぐもの。展開の数が上限(例:256)を超えたら拒否する。
展開した各文字列と元の文字列の両方 について、今の検査(~・$・バッククォート・.. の component・親の証明)を行う。これで {.,}./f は .. として拒否される。
glob は component ごとに展開する :root から 1 component ずつ、symlink をたどらずに名前を列挙して一致を探す。component が . で始まるときだけ . と .. も候補に入れ、.. に一致したら拒否する。一致した途中の path が root の外に出たら、その下を列挙せずにすぐ拒否する(外のディレクトリを読まない)。一致の合計が上限(例:4096)を超えたら拒否する(正直な失敗)。一致しないものは、bash と同じく元の文字列として 3 で確かめる。
一致の判定には既存の依存の globset を component 単位で使ってよい([!x] と [^x]、** は sh と同じく component 単位の *)。Cargo の依存は増やさない。
ロジックは新しい子モジュールに置き、path_guard.rs には呼び出しの数行だけを足す。bash_write_guard.rs と bash.rs は変えない([security][tools] Bash の書き込み検査が >| と数字でない >& を redirect として読まず、ワークスペース外へ書き込める(#509 の調査で発見) #565 と重ならない)。
残る限界(report に書く):extglob・GLOBIGNORE・nocaseglob(sh の既定では無効)、判定と実行の間の symlink の差し替え(今の証明と同じ限界)。まれに、引用符の中の [id] の i や d が外を指す symlink だと、bash は展開しないのに拒否する(保守側の誤拒否)。
対象ファイル
src/tools/path_guard.rs — ensure_bash_write_target の入口で展開の検査を呼ぶ(数行)。test module に表を足す
src/tools/path_guard/bash_pattern.rs — 新規。brace の展開、component ごとの glob の一致、上限、外に出たときの打ち切り
tests/issue567_bash_glob_write_targets.rs — 新規。path_confinement_rejection を呼ぶ結合テスト
受け入れ条件
テストの設計(参考)
fixture:ws の中に linked-outside(外を指す)、.hidden-out(外を指す)、sub/loop(sub を指すループ)、src/[id]/。外には既存の secret を置く。
拒否する形:
glob:tee lin*/f、tee linked-outsid?/f、tee linked-outsid[e]/f、tee lin*/secret、cp a.txt lin*/、chmod 777 lin*/secret、tee .hid*/f、tee */secret、tee */secret、tee $ROOT/lin /f($ROOT は root の絶対パスを埋め込む)
.. になる形:tee .*/f、tee .?/f、tee {.,}./f、tee {.,.}./f
brace:tee {linked-outside,x}/f、touch {linked-outside,x}/f、tee {k..m}inked-outside/f、tee {linked-outside/f,x}、printf x > {linked-outside,x}/f
上限:{a,b} を 12 個並べた brace、一致が上限 + 1 個になるディレクトリ
読み取りの brace(この設計で塞がる):cat {linked-outside,x}/secret、cat $ROOT/lin*/secret、cat {.,}./secret
許可のままの形:
tee out.txt、tee src/new.rs、tee sub/f、tee nomatch*/f(一致なし)、tee /f(中のディレクトリだけに一致)、tee sub/loop/loop/ /f(ループでも終わる)
tee "src/app/[id]/page.tsx"、mkdir -p 'src/app/[id]'、printf x > 'src/[id]/route.ts'、printf x > "src/{a,b}.txt"、printf x > /dev/null
読み取り:cat "src/[id]/route.ts"、cat src/{main,lib}.rs、ls $ROOT/src/.rs、ls src/ .rs、rg foo src/**/*.rs、cat *.txt
回帰で通す既存の結合テスト(参考)
作業の場所(参考)
ビルドとテストは CARGO_TARGET_DIR=/Volumes/SSD_NX/tmp/parallel-565-567-20261001-01/target-567 と CARGO_INCREMENTAL=0 で行います。
変異テストは TMPDIR と -o を /Volumes/SSD_NX/tmp/parallel-565-567-20261001-01/ の下に置き、--file に新しい子モジュールを指定します。
symlink と fixture はテストの tempdir の中だけで作り、ワークスペースの外への実際の書き込みは試しません。
着手前調査の report:ローカルの workspace/tmp/1001/parallel-565-567/results/H-01/report.md(§1・§3.2)。再現用の probe:/Volumes/SSD_NX/tmp/parallel-565-567-20261001-01/probe。
背景
#509(bash_write_guard の変異テスト)の着手前調査(2026-09-30)で見つけた見逃しです。2026-10-01 の着手前調査(develop@ad9ff41)で、原因と直し方を確かめ、Issue に書いていなかった形も見つけました。判定の関数を呼んで結果を確かめ、shell の挙動は scratchpad の中の /bin/sh(bash 3.2、POSIX モード)でだけ確かめています。
問題(背景)
書き込み先の glob(
* ? [)と brace({a,b}・{a..z})を、展開前の文字列のまま判定します。展開前の名前は存在しないので、未作成の扱いで親(ワークスペースの root)まで戻り、内側と判定されます。実行時の展開で、中の symlink(外を指す)や親ディレクトリ(..)に届きます。tee lin*/secret、cp a.txt lin*/、chmod 777 lin*/secrettee {linked-outside,x}/f、tee {k..m}inked-outside/f、tee {linked-outside/f,x}tee .*/f、tee .?/f、tee {.,}./f、tee {.,.}./f..になる)影響(背景)
/tmp に限らず、symlink の先や親ディレクトリへ書き込めます。
着手前調査の結果(背景。2026-10-01)
決めた設計(2026-10-01)
,、連番({a..z}・{1..9})、/をまたぐもの。展開の数が上限(例:256)を超えたら拒否する。~・$・バッククォート・..の component・親の証明)を行う。これで{.,}./fは..として拒否される。.で始まるときだけ.と..も候補に入れ、..に一致したら拒否する。一致した途中の path が root の外に出たら、その下を列挙せずにすぐ拒否する(外のディレクトリを読まない)。一致の合計が上限(例:4096)を超えたら拒否する(正直な失敗)。一致しないものは、bash と同じく元の文字列として 3 で確かめる。[!x]と[^x]、**は sh と同じく component 単位の*)。Cargo の依存は増やさない。>|と数字でない>&を redirect として読まず、ワークスペース外へ書き込める(#509 の調査で発見) #565 と重ならない)。対象ファイル
src/tools/path_guard.rs— ensure_bash_write_target の入口で展開の検査を呼ぶ(数行)。test module に表を足すsrc/tools/path_guard/bash_pattern.rs— 新規。brace の展開、component ごとの glob の一致、上限、外に出たときの打ち切りtests/issue567_bash_glob_write_targets.rs— 新規。path_confinement_rejection を呼ぶ結合テスト受け入れ条件
テストの設計(参考)
fixture:ws の中に linked-outside(外を指す)、.hidden-out(外を指す)、sub/loop(sub を指すループ)、src/[id]/。外には既存の secret を置く。
拒否する形:
..になる形:tee .*/f、tee .?/f、tee {.,}./f、tee {.,.}./f許可のままの形:
回帰で通す既存の結合テスト(参考)
>|と数字でない>&を redirect として読まず、ワークスペース外へ書き込める(#509 の調査で発見) #565 が同時に変更するので、[security][tools] Bash の書き込み先の glob・brace を展開前の文字列で判定するため、中の symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #567 では触らない)作業の場所(参考)
根拠(develop@ad9ff41)