Repository navigation
[security][tools] Bash の閉じ込めの検査が、引用していない CR を区切りとして扱い、/ を含まない相対の語を候補にしないため、外向き symlink を通して外を読み書きできる(#602・#603 のレビューで発見) #613
Description
背景
#602・#603 の merge 前レビュー(H-06、2026-10-08)で見つけた、Bash の閉じ込めの検査の既存の見逃し 2 種類です。#602・#603 の前の develop にもあり、この 2 本では直していません。
着手前調査(H-09、2026-10-08)で、#602・#603 の merge 後の develop(e7c17f6c)を基準に、判定の関数を tempdir の fixture に対して呼んで確かめ、設計を 1 つに絞りました。shell は起動しておらず、ワークスペースの外の実際の読み書きは試していません。
問題
値は R A W P S V の順(R:2 段目の閉じ込めの判定、A:書き込みと認識、W:書き込み側の拒否、P:保護 path、S:秘密情報、V:bash:verify の自動許可)。⏎ は CR(\r)。
1. 引用していない CR
bash は CR で語を分けません(語を分けるのは空白・タブ・改行と演算子だけ)。読み取り側と書き込み側の字句解析は、CR を改行と同じ区切りとして扱うので、外向き symlink の名前に CR が入っていると、検査は別の語を確かめ、外を読み書きできます。
fixture:ワークスペースの中の sub/cr⏎link と cr⏎dir は外のディレクトリを指す symlink、lfcr⏎(名前の最後が CR)は外のファイルを指す symlink。
| 入力(例) | 基準 e7c17f6c |
期待 |
|---|---|---|
echo x > sub/cr⏎link/f |
0 1 0 0 0 0 | R=1・W=1 |
cat sub/cr⏎link/secret |
0 0 0 0 0 1 | R=1 |
cd cr⏎dir && echo x > f |
0 1 0 0 0 0 | R=1・W=1 |
tee sub/cr⏎link/f、cp a.txt sub/cr⏎link/f |
0 1 0 0 0 0 | R=1・W=1 |
cd cr⏎dir; touch f、pushd cr⏎dir && touch f |
0 1 0 0 0 0 | R=1・W=1 |
cd cr⏎dir && cat secret |
0 0 0 0 0 0 | R=1 |
cat sub/cr⏎link/secret⏎+改行(CRLF の行末) |
0 0 0 0 0 1 | R=1 |
cat lfcr⏎+改行(CRLF の行末) |
0 0 0 0 0 1 | R=1(設計 2 も要る) |
2. / を含まない相対の語
静的な path の判定が / を必須にしているので、/ を含まない相対の語は候補になりません。その語自体が外を指す symlink だと、外を読めます。cd があるコマンドでだけ、別の経路(作業ディレクトリの候補の検査)が / のない語を確かめています(cd sub && cat lf は今も R=1)。
fixture:ワークスペースの中の lf・space lf・wide lf(全角空白)・linked-outside は外を指す symlink、dangling は行き先のない symlink。
| 入力(例) | 基準 e7c17f6c |
期待 |
|---|---|---|
cat lf、head -n1 lf、grep x lf、diff a.txt lf |
0 0 0 0 0 1 | R=1 |
cat "space lf"、cat space\ lf、cat wide lf |
0 0 0 0 0 1(エスケープは 0 0 0 0 0 0) | R=1 |
dd if=lf、grep --file=lf a.txt、make IN=lf |
0 0 0 0 0 1 | R=1 |
cat < lf、cat lf | head、(cat lf)、true && cat lf |
0 0 0 0 0 1 または 0 0 0 0 0 0 | R=1 |
cp lf a2.txt |
0 1 0 0 0 0 | R=1 |
source lf、. lf、python3 lf |
0 0 0 0 0 1 | R=1 |
ls linked-outside、cat dangling |
0 0 0 0 0 1 | R=1 |
書き込み側(echo x > lf、tee lf、cp a.txt lf)は今も W=1 で、この Issue では変えません。
影響
ワークスペースの中の symlink を通して、外のファイルを読めます。1 は外に書き込むこともできます。Gate 1 の閉じ込めの約束が、字句の形によって守られません。
設計(H-09 で決めたもの)
設計 1:CR を語の文字とする読み方を足し、今の読み方と両方で判定する
3 案を比べました。
| 案 | 内容 | 判断 |
|---|---|---|
| A:CR を語の文字にするだけ | bash と同じ読み方に置き換える | 取らない。true⏎rm 保護 path の P と A が 1→0、V が 0→1 になり、true⏎tee 外の絶対パス の A・W が 1→0、V が 0→1 になる(bash では true⏎rm というコマンドが見つからず何も起きないが、印が緩む向きに変わる) |
| B:両方の読み方で判定する | CR を語の文字とする読み方(bash と同じ)を足し、今の読み方(CR を区切りとする)も残して、どちらかで拒否・認識したら拒否・認識とする | 推奨 |
| C:引用していない CR を一律に拒否する | CR を含むコマンドを、閉じ込めを証明できないとして拒否する | 取らない。最も単純だが、CRLF の行末のコマンドをすべて拒否する方針の変更になる(記録では 0 件) |
- 読み取り側:
read_wordsに読み方を選ぶ引数を足し、read_guard::inspectは 2 つの読み方の候補を合わせ、確かめられない語はどちらかで見つかれば拒否する。 - 書き込み側:
lex_shellに同じ引数を足し、書き込み先(write_targets、raw_write_targets)を 2 つの読み方で集めて合わせる。has_recognized_mutation・protected_path_mutation・confinement_rejectionは、この合わせた書き込み先から求めるので、どれも今より緩まない。 - 作業ディレクトリの候補:
working_directory::inspectも 2 つの読み方で求め、候補・cdの行き先・/のない語を合わせ、判定できない印はどちらかが立てば立てる。 - 2 つ目の読み方は、引用の外に CR があるときだけ要る(ないときは 2 つの読み方が同じになる)。
- CRLF の行末:CR+LF を 1 つの改行に直さない。bash と同じく、CR は直前の語の最後の文字として残す(今の読み方では CR を除いた語として確かめる)。直すと、名前の最後が CR の外向き symlink(
lfcr⏎)を見逃す。存在しない名前(a.txt⏎)や、親がワークスペースの中の名前(sub/f⏎)は今どおり許可。 - 改行(LF)・空白・タブは今どおり区切り。全角空白などの扱い([security][tools] Bash の書き込み先で、引用した閉じ記号と実行される glob・brace が混ざると、外向き symlink の先への書き込みを拒否しない(#582 のレビューで発見) #602・[security][tools] Bash の 2 段目の path 検査が、空白を含む静的パスと = 付きの引数を候補にせず、外を読める(#582 のレビューで発見) #603)は変えない。秘密情報・隠し path・構造の検査が CR を区切りとして扱っている所は、そのまま残す(厳しい側)。
設計 2:/ を含まない語は、symlink のときだけ確かめる(案 A)
2 案を比べました。
| 比べる点 | 案 A:symlink のときだけ確かめる(推奨) | 案 B:存在すればすべて確かめる |
|---|---|---|
| 見逃し | 表 2 をすべて拒否 | 案 A と同じ(普通のファイル・ディレクトリは、親が中なら必ず中に解決するので差がない) |
| 誤拒否:外向き symlink と同じ名前のコマンド名・文字列 | 拒否する(node x.js、echo node。その名前の外向き symlink があるときだけ) |
案 A と同じ |
| 誤拒否:存在しない語 | なし | 引用した $ を含む語を、動的な path として拒否する(awk -F= '{print $1}' a.txt が 0→1)。ほかにも、今の確かめ方の字面の検査に当たる語を拒否しうる |
| ファイルシステムを見る回数 | 語 × 作業ディレクトリの候補ごとに lstat 1 回。symlink のときだけ解決する | 存在する語ごとに解決(realpath)、存在しない語は親をたどる |
| 速さ(試作、1030 件、debug) | 0.14 秒 → 0.20 秒(設計 1 の 2 つの読み方を含む) | 0.25 秒 |
| TOCTOU | 確かめた後に名前が symlink に変わる形は見逃す(今の / を含む候補と同じ。静的な検査の限界) |
案 A と同じ |
| 作業ディレクトリの候補 | ワークスペースの直下と、決まった各 cd の行き先で確かめる。行き先を決められないときは、今の相対の候補と同じく拒否する |
同じ |
= の右側(#603 の設計 3) |
右側に / がないときも同じ形で確かめる(dd if=lf)。/ があるときは #603 の扱いのまま |
同じ |
- 対象の語:展開(
$・バッククォート)も glob もない静的な語で、引用を外した文字列に/がないもの。空白・全角空白・CR を含んでよい。除くもの:空の語、.、..、~で始まる語、segment の先頭の代入(NAME=値)。コマンド名の位置とオプションは除かない(cdがある時の今の検査と同じ。symlink でなければ判定に影響しない)。 =の右側:先頭の代入でない語の最初の=の右側が、空でなく/を含まないなら、同じ対象として確かめる。- 確かめ方:ワークスペースの直下と各作業ディレクトリの候補に名前をつないだ path を lstat し、symlink なら(または、存在しない・ディレクトリでない以外の理由で lstat に失敗したら)、今の literal の確かめ方(解決して中にあるか。行き先がなければ拒否)で確かめる。symlink でない・存在しないなら今どおり許可。
- 書き込み側は変えない(
/を含まない書き込み先は今も確かめている)。
試作での確認(H-09)
設計 1(案 B)と設計 2(案 A)を試作し、1030 件(新しい形 138 件、H-03・H-05・H-06 の形、記録の 436 件)を流しました。値が変わったのは 76 件で、すべて厳しい向き(R か W が 0→1)でした。緩む向きの変化は 0 件です。
誤拒否の見積り
- 対象:
dev-reportsとworkspace/managementの jsonl(495 ファイル)に残る agent の Bash コマンド 436 件(重複なし)。 - CR を含むもの:0 件(jsonl に CR も
\rの escape も 0 件)。設計 1 の誤拒否は 0 件。 /を含まない語を引数に持つもの:335 件(138 種類。多いのは-la・-f・2・-p・f・src、npm の package 定義のファイル名など)。すべて lstat の対象になるが、拒否されるのは、その名前がワークスペースの中の外向き symlink のときだけ。- 記録にはファイルシステムの状態がないので、記録に残る 22 個の作業用ワークスペースの symlink を数えた:320 個で、すべて npm の実行ファイルのディレクトリ(node_modules の下の bin)の中で、
/を含む path、外向きは 0 個、ワークスペースの直下には 0 個。よって推奨案の誤拒否は 0 件と見積もった。 - fixture の直下に外向きの
nodesymlink を置いた試作では、記録の 9 件(node -p …、node smoke-check.jsなど)が拒否された。これが、外向き symlink と同じ名前のコマンドを使うときの誤拒否の形で、閉じ込めの約束として受け入れる。
許可のまま(誤って拒否しない形)
| 入力(例) | 基準の値 |
|---|---|
cat README.md、cargo test、ls src、ls -la、git status |
0 0 0 0 0 1 |
echo x > out.txt、touch newfile、cp a.txt c.txt |
0 1 0 0 0 0 |
cd sub && cat f、cd sub && ls |
0 0 0 0 0 0 |
make CC=gcc、cargo build --features=gui、dd if=input.txt of=out.bin |
0 0 0 0 0 1 |
cat inlink(中を指す symlink)、echo node(外向きの node がないとき) |
0 0 0 0 0 1 |
awk -F= '{print $1}' a.txt、FOO=lf cargo test |
0 0 0 0 0 1 |
CRLF の行末:cat a.txt⏎+改行、cargo test⏎+改行、git status⏎+改行git diff⏎+改行 |
0 0 0 0 0 1 |
CRLF の行末:cd sub⏎+改行cat f⏎+改行、echo x > out.txt⏎+改行 |
0 0 0 0 0 0、0 1 0 0 0 0 |
受け入れ条件
- 問題の 2 つの表のすべての形が、期待どおり拒否される。
- 「許可のまま」の表の形を誤って拒否しない。
-
true⏎rm 保護 path(今の値 0 1 0 1 0 0)とtrue⏎tee 外の絶対パス(今の値 1 1 1 0 0 0)の値が変わらない。 - 保護 path、秘密情報、bash:verify の自動許可、[security][tools] Bash の書き込み検査の字句解析がバッククオートのコマンド置換を扱わず、中の書き込みを見逃してワークスペース外へ書き込める(#576 の調査で発見) #581・[security][tools] Bash の字句解析が二重引用の中の $(…)・${…} の " を引用の閉じと読み違え、後ろをコメントとして消してワークスペース外へ書き込める(#581 のレビューで発見) #585・[security][tools] Bash の 2 段目の path 検査が、$・glob・動的な値で静的な相対パスや展開先を確かめず、ワークスペース外を読める(#571 を統合) #582・[security][tools] Bash の書き込み先で、引用した閉じ記号と実行される glob・brace が混ざると、外向き symlink の先への書き込みを拒否しない(#582 のレビューで発見) #602・[security][tools] Bash の 2 段目の path 検査が、空白を含む静的パスと = 付きの引数を候補にせず、外を読める(#582 のレビューで発見) #603 の印と判定が緩む向きに変わらない。
- 既存の検証・受入・証跡・リリースゲートを緩めていない(AGENTS.md の honest-failure 方針)。成長上限の baseline を引き上げていない。
対象ファイル
src/tools/bash/path_tokens.rssrc/tools/bash/read_guard.rssrc/tools/bash.rssrc/tools/bash_write_guard.rssrc/tools/bash_write_guard/working_directory.rstests/issue582_bash_read_candidates.rstests/issue567_bash_glob_write_targets.rstests/corpus/apps/issue582-bash-read-candidates/fixtures/decision-cases.jsonl
参照
- 基準は develop の
e7c17f6c([security][tools] Bash の書き込み先で、引用した閉じ記号と実行される glob・brace が混ざると、外向き symlink の先への書き込みを拒否しない(#582 のレビューで発見) #602・[security][tools] Bash の 2 段目の path 検査が、空白を含む静的パスと = 付きの引数を候補にせず、外を読める(#582 のレビューで発見) #603 の merge 後)。 - 判定の表は
tests/corpus/apps/issue582-bash-read-candidates/fixtures/decision-cases.jsonlに行を足す。この表はsrc/tools/bash/read_guard.rsの単体テストとtests/issue582_bash_read_candidates.rsの両方が読むので、両方の fixture に、名前に CR を含む symlink(sub/cr⏎link、cr⏎dir、lfcr⏎)と、直下の外向き symlink(lf、space lf)を足す。 - 受け入れ条件の「保護 path」は、既存の fixture の保護 path(
tests/spec.rs)。 src/tools/bash.rsの変更は、/を含まない語の候補を確かめる呼び出しだけにする(確かめ方の本体はsrc/tools/bash/read_guard.rsに置く)。- 成長上限:
docs/dev/dev-guardrails.mdの baseline の一覧に、この Issue で変えるファイルは入っていない。src/planner/runner.rsとsrc/minimal_loop/loop_run.rsは変えない。 - 変えないファイル:
src/tools/path_guard.rs、src/tools/shell_lexical.rs、src/tools/sensitive_path.rs、src/tools/hidden_path.rs、src/tools/allow_policy.rs、src/planner/verify.rs。 - この Issue の範囲外(別の Issue の候補):
ls ..・ls ~が R=0(ワークスペースの外の一覧を読める)。dd of=…が書き込みと認識されない(A=0、V=1)。
根拠(基準コミット e7c17f6c)
- 読み取り側で CR を区切りにする:
src/tools/bash/path_tokens.rs:156 - 書き込み側で CR を区切りにする:
src/tools/bash_write_guard.rs:729 - 静的な path に
/を必須にする:src/tools/bash/path_tokens.rs:359、使う側:src/tools/bash/read_guard.rs:122-153(push_static)、:158-165(equals_right_hand_side) - 候補を確かめる所:
src/tools/bash.rs:796-835 cdがあるときだけ/のない語を確かめる:src/tools/bash_write_guard/working_directory.rs:141-147・:244-253- 書き込み先の集め方:
src/tools/bash_write_guard.rs:223-227・:229-260・:309-361 - 引用した
$を含む語を動的として拒否する確かめ方(案 B の誤拒否の原因):src/tools/path_guard.rs:230-238 - 自動許可が
has_recognized_mutationに頼る:src/tools/allow_policy.rs:122-142 - 調査の記録:ローカルの
workspace/tmp/1007/wave1/results/H-09/report.md(proto.diff、h09_probe.rs、cases.py)
関連(参考)
- [security][tools] Bash の書き込み先で、引用した閉じ記号と実行される glob・brace が混ざると、外向き symlink の先への書き込みを拒否しない(#582 のレビューで発見) #602
- [security][tools] Bash の 2 段目の path 検査が、空白を含む静的パスと = 付きの引数を候補にせず、外を読める(#582 のレビューで発見) #603
- [security][tools] Bash の 2 段目の path 検査が、$・glob・動的な値で静的な相対パスや展開先を確かめず、ワークスペース外を読める(#571 を統合) #582
- [security][tools] Bash の書き込み検査が同じコマンドの cd・pushd の後の cwd を追わず、symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) #568
- [security][tools] Bash の字句検査では、プログラム経由の間接書き込みや検証コマンドの副作用を閉じ込められない #502
Activity
Merge pull request #636 from Kewton/feature/issue-613-security-tools-…
Kewton commentedon Oct 9, 2026
PR #636 で develop に merge しました(merge commit 2a078de、CI・acceptance・Next.js domain oracles success)。引用していない CR を語の文字とする読み方を足して両方で判定し、/ を含まない相対の語は symlink のときだけ確かめるようにしました。merge 前レビュー(H-10)は blocker 0。レビューで見つかった既存の穴は #635・#628 で扱います。