Skip to content

[security][tools] Bash の書き込み先の glob・brace を展開前の文字列で判定するため、中の symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #567

Description

@Kewton

背景

#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)

  1. 展開して、一致したものをすべて今の証明で確かめる。 raw が glob・brace の文字を含まなければ今のとおり(変更なし)。
  2. brace を先に文字列として展開する:入れ子、,、連番({a..z}・{1..9})、/ をまたぐもの。展開の数が上限(例:256)を超えたら拒否する。
  3. 展開した各文字列と元の文字列の両方について、今の検査(~・$・バッククォート・.. の component・親の証明)を行う。これで {.,}./f は .. として拒否される。
  4. glob は component ごとに展開する:root から 1 component ずつ、symlink をたどらずに名前を列挙して一致を探す。component が . で始まるときだけ . と .. も候補に入れ、.. に一致したら拒否する。一致した途中の path が root の外に出たら、その下を列挙せずにすぐ拒否する(外のディレクトリを読まない)。一致の合計が上限(例:4096)を超えたら拒否する(正直な失敗)。一致しないものは、bash と同じく元の文字列として 3 で確かめる。
  5. 一致の判定には既存の依存の globset を component 単位で使ってよい([!x] と [^x]、** は sh と同じく component 単位の *)。Cargo の依存は増やさない。
  6. ロジックは新しい子モジュールに置き、path_guard.rs には呼び出しの数行だけを足す。bash_write_guard.rs と bash.rs は変えない([security][tools] Bash の書き込み検査が >| と数字でない >& を redirect として読まず、ワークスペース外へ書き込める(#509 の調査で発見) #565 と重ならない)。
  7. 残る限界(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 を呼ぶ結合テスト

受け入れ条件

  • 下の「拒否する形」がすべて拒否される(問題の表の形を含む)
  • 下の「許可のままの形」が許可のままである。下の「回帰で通す既存の結合テスト(参考)」は変更せず、そのまま通る
  • 上限(brace の展開の数、glob の一致の合計)を超えると拒否され、symlink のループで終わらなくならないことをテストで確かめた
  • 直す前の develop で新しいテストが失敗し、直した後で通ることを report に記録した(二点測定)
  • 新しい子モジュールだけで cargo mutants を実行し(上限 30 分)、生き残った変異に理由を添えて report に書いた
  • 既存の検証・受入・証跡・リリースゲートを緩めていない(AGENTS.md の honest-failure 方針)
  • 成長上限(docs/dev/dev-guardrails.md)の baseline を引き上げていない

テストの設計(参考)

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。

根拠(develop@ad9ff41)

Activity

  1. added 2 commits that reference this issue on Oct 1, 2026
  2. Kewton commented on Oct 1, 2026

    @Kewton
    OwnerAuthor

    PR #573(merge commit 87f7a77)で対応しました。書き込み先の brace と glob を展開し、一致したものすべてを既存の証明で確かめます(外に出る一致は下を列挙せずに拒否、.. になる形も拒否、展開の上限は展開前に効かせる)。sh と書き方の解釈が違う形(POSIX の文字クラス、[ と { } , \\ の混在)は、確かめられない書き方として拒否します。merge 前レビューで見つかった blocker 2 件は直した後に再確認済みで、15 ゲートは 15/15 pass です。読み取りの抜け道は #571 で扱います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions