AIシステムにおけるBashコマンド安全性解析はどのように機能するか

Dev.to / 2026/4/6

💬 オピニオンDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

要点

  • この記事は、AIシステムにおけるシェルコマンドの「安全性バリデーション」は、単純なブロックリストに頼ることはできないと論じる。理由は、Bash構文の曖昧さ(例:区切りと引用されたテキストの違い)がバイパスを可能にするためである。
  • それに代えて、フェイルクローズ(クローズ寄り)の、許可リスト(allowlist)ベースの設計を提案する。すなわち、十分に理解され、既知で安全な構文のみを承認し、それ以外はすべてユーザーの明示的な確認を必要とする。
  • 多層の解析パイプラインの概要を示す。まず事前パースチェックを行い、その後、AST(抽象構文木)へと構造化パースする。続いて、許可リストの走査、変数スコープの追跡、プレースホルダ制御、セマンティック(意味的)バリデーション、ファイルシステム/パスのチェック、そして最終的なポリシー適用を行う。
  • 信頼できるシステムには、生のコマンドテキストだけでなく、コマンド構造とシェルのセマンティクス(引用、展開、置換、算術、ブレース/プロセス関連の機能など)を推論することが不可欠だと強調する。
  • この議論は、特定の製品実装のドキュメントではなく、AI支援型の評価器をどのように設計し得るかを、クリーンルーム方式で概念的に再構成したものである。

多くの人は、シェルコマンドの検証は簡単だと思っています。rmを探し、evalをブロックして、それで終わり。そんな単純な話ではありません。

ここで述べるのは、AI支援システムが、実行前にbashコマンドの安全性をどのように評価し得るかを、クリーンルームの技術的な観点から再構成したものです。内容はすべて、外部から観測可能な振る舞い、公開されている技術パターン、および一般的なシェルのセマンティクスに基づいています。独自のソースコードや内部資料にはアクセスしていません。

記述されているすべての仕組みは概念的な再構成であり、特定の実装のドキュメントではなく、そのようなシステムがどのように設計できるかを説明するものです。

根本的な問題

一見すると、シェルコマンドの検証は簡単に見えます。危険なパターン — rmeval;、パイプ、リダイレクト — をスキャンして、それらをブロックする、というやり方です。

しかし、このアプローチは即座に破綻します。

例えば:

echo "safe" ; rm -rf /

一方で:

echo "safe ; rm -rf /"

表面的なパーサでは、;がコマンド区切りなのか、引用符で囲まれた文字列の一部なのかを区別できません。

シェル構文には、クォートのルール、変数展開、コマンド置換、算術評価、ブレース展開、プロセス置換が含まれます。これらのいずれかによって、一見無害に見えるテキストが危険な実行へと変換され得ます。

信頼できるシステムは、コマンドの構造を理解する必要があり、単にテキストを見るだけでは不十分です。

設計原則: fail closed

頑健なアナライザは、厳格なルールに従います。あるコマンドを完全に理解できないなら、自動的に承認してはならない。

このことは、許可リスト(allowlist)ベースの設計につながります。既知の安全な構成要素だけを受け入れ、それ以外はすべて「複雑すぎる」とみなしてユーザー確認が必要になります。

これにより、ブロックリスト(blocklist)の根本的な弱点 — 新しい攻撃経路が出るたびに自動的なバイパスになってしまう — を回避できます。許可リストでは、新しい構成要素が登場するたびにプロンプトが発生します。

マルチレイヤのパイプライン

適切に設計されたシステムは、複数の防御レイヤを順に適用してコマンドを処理します。それぞれが異なる種類の失敗に対処します:

  1. 事前パース検証
  2. 構造化パース(AST)
  3. 許可リストに基づくトラバース
  4. 変数スコープの追跡
  5. 制御されたプレースホルダシステム
  6. セマンティクス(意味)検証
  7. パスおよびファイルシステムのチェック
  8. ポリシー適用

それぞれ見ていきましょう。

レイヤ1: 事前パース検証

パースの前に、生のコマンド文字列を検査し、パーサが見ているものとシェルが実行しているものの間に曖昧さが生まれるパターンを探します。

制御文字。 隠れたバイトは、テキストの解釈方法を変えます。ヌルバイト、バックスペースのシーケンス、ANSIエスケープコードは、端末上では別の見え方をしているのに、パーサには別のコマンドとして見えるようにできます。

見えないUnicode。 ゼロ幅スペースのような文字は、視覚的にコマンドを偽装できます。見た目がlsでも、実際には実行を変える不可視の文字が含まれているかもしれません。

バックスラッシュによる行継続。 これは微妙です:

tr\
aceroute

トークンが2つに見えますが、実際の実行結果はtracerouteです。継続を扱えないパーサは、シェルとは別のものを見てしまいます。

シェル固有の拡張。 zshの機能はbashのパース規則と一致しないことがあります。アナライザがbashのセマンティクスを前提にすると、zsh固有の構文が曖昧さを作ります。

ブレースによる難読化。 {} 内の複雑なクォートは、単純なパーサを誤認させます。

このレイヤの目的は、異なる解釈者がコマンドの意味について意見を食い違わせるような入力を排除することです。

レイヤ2: 構造化パース

正規表現の代わりに、システムは構文木 — AST(抽象構文木) — を構築します。

これにより、実行せずにコマンドを切り分け、引数を特定し、構造を追跡できます。しかし、パースだけでは不十分です。パーサは、解析対象のコマンドを決して実行してはいけません。

ここではリソース制限も重要です。最大入力サイズ、最大パース複雑度、厳格な時間制限。いずれかを超えた場合、そのコマンドは「複雑すぎる」とマークされます。これにより、パーサをハングさせるために設計された敵対的入力を防ぎます。

レイヤ3: 許可リストによるASTトラバース

パース後、システムは構文木を歩きます。重要なルール: 明示的にサポートされたノード型のみ許可します。

サポートされる構成には、単純なコマンド、パイプライン、条件分岐、変数の代入などが含まれます。ウォーカーが認識しないもの — すべての未知のノード型 — は、直ちに「複雑すぎる」と分類されます。

これは、パイプライン全体の中でも最も重要な設計判断です。危険なパターンをすべて列挙する必要がなくなるためです。安全なものだけを列挙すればよい、ということになります。

レイヤ4: 変数スコープの追跡

シェルの振る舞いは実行順序に大きく依存し、ここで素朴なアナライザが破綻します。

true || FLAG=--safe && cmd $FLAG

素朴なシステムは$FLAGが常に設定されていると見なしてしまうかもしれません。しかし実際には、||&&のチェーンのつながり方によってはFLAGが一度も代入されない可能性があります。

アナライザは分岐(&&, ||)、サブシェル(())、パイプラインをモデル化します。変数は正しい実行セマンティクスに従って追跡されます — 変数がすべての分岐で定義される可能性がないなら、その変数は未知として扱います。

レイヤ5: プレースホルダシステム

いくつかの構成は、静的に解決できません。

echo "commit $(git rev-parse HEAD)"

コマンド全体を拒否するのではなく、外側のコマンドは保持し、内側のコマンドを抽出して別途解析します。

__CMDSUB_OUTPUT__ のようなプレースホルダ(コマンド置換用)と、__TRACKED_VAR__ のようなプレースホルダ(未知の変数用)により、実行時の値を知らなくても、アナライザはコマンドの構造について推論できます。

ただし重要な制約があります — 生の変数リスク(bare variable risk):

VAR="-rf /"
rm $VAR

シェルはこれをrm -rf /へと展開します。これを防ぐため、空白やグロブパターンを含む変数は、引用符で囲まれていない限り拒否します。$VAR"$VAR" の引用符の違いが、成立するかどうかを左右します。

レイヤ6: セマンティック検証

構文的に正しく、構造的にも理解できたコマンドでも危険になり得ます。

evalのような振る舞い。 文字列をコードとして実行するコマンド — evalsourceexec — は、アナライザが見えない実行時の値に挙動が依存するため、本質的に危険です。

間接的な実行。 トラップ、動的ローディング、サブシェルのトリガーは、一見安全に見える操作の副作用としてコードを実行してしまうことがあります。

ツールへの埋め込み実行。 これはずるいやつです:

jq 'system("rm -rf /")'

外側のコマンドは jq です。内側のペイロードは任意のシェル実行です。独自の式言語を持つ任意のツール — awkperljqfind -exec — はベクターになり得ます。

添字の評価。 一部のシェル式は、評価中に実行を引き起こします:

test -v 'a[$(cmd)]'

配列の添字が評価され、その過程で cmd が実行されます。これは実際の bash の挙動で、ほとんどの人は知りません。

レイヤー 7: ファイルシステムとパスの安全性

コマンドは、ファイルシステムへの影響によって分類されます。読み取り、書き込み、または破壊的操作です。

特定のパスは常に機密性が高く — /etc/usr/bin/proc — 明示的な承認が必要で、どのコマンドであっても例外はありません。

ここでは特殊ケースが重要です。-- 区切り(rm -- -file)は、フラグとして誤解釈してはいけません。プロセス置換(>(command))は副作用を隠すことができます。ディレクトリ変更の後に書き込みが続く場合(cd dir && write file)、実行追跡がないと曖昧さが生まれます。

レイヤー 8: ポリシーの強制

すべての分析の後、コマンドは設定可能なルールに照らして評価され、次の3つの結果のいずれかになります:

  • 許可(Allow) — 安全で、完全に理解できる
  • 質問(Ask) — 不明確、複雑、または境界領域
  • 拒否(Deny) — 明示的に禁止

ルールは完全一致、プレフィックス一致、またはパターン一致で照合できます。システムはさらに、基盤となるコマンドを分析するために timeoutenv のようなラッパーを取り除き、複合コマンドを検出し、セグメント間の分析を行います。たとえ cd dirgit status がそれぞれ個別には安全でも、それらの組み合わせは安全とは限らないからです。

重要な洞察

このシステムは、コマンドが安全であることを証明しようとはしません。より狭い問いに答えます:

「高い確信度で、このコマンドを完全に理解できますか?」

答えが「いいえ」なら、ユーザーに確認します。

レイヤー 目的
事前チェック 曖昧さを取り除く
パース 構造を理解する
AST の許可リスト 未知の構造を拒否する
スコープ追跡 実行セマンティクスを保持する
プレースホルダー 動的な振る舞いを扱う
セマンティクス 危険な意図を検出する
パスの検証 ファイルシステムを保護する
ルール ポリシーを強制する

8つのレイヤーです。それぞれが、他では見落とすものを捕まえます。設計は賢ぶっていません — きちんと徹底しています。そして、不確実性に対するデフォルトの答えは常に同じです:実行しない。質問する。

これは覚えておく価値のある、より広い原則です。不確実性がデフォルトで実行に向かうべきでは決してありません。

X でフォローしてください — @oldeucryptoboi として投稿しています。