出力境界エンコーディング
モデルが出力するすべてのものを、それを消費するシステムに対する敵対的な入力として扱います。レンダラー、シェル、クエリ、下流のエージェントなど、各宛先において、その宛先独自のルールを使用してエンコードと検証を行います。単一のグローバルなサニタイザーではこれを行うことはできません。HTMLにとって正しいエスケープ処理は、シェルにとっては無意味だからです。
定義
出力境界エンコーディングとは、モデルの出力がそれを解釈するシステムに渡されるすべてのポイントにおいて、モデルの出力時に一度だけ汎用的なフィルタリングを行うのではなく、宛先固有のエンコードと検証を適用するプラティスです。これが欠如している状態は、OWASPが「不適切な出力処理(improper output handling)」と呼ぶ脆弱性に該当します。
課題
開発チームはプロンプトインジェクションに対して入力側を堅牢化する一方で、出力側を無防備なままにしがちです。その結果、モデルのテキストがレンダラー、シェル、データベース、または別のエージェントのコンテキストに到達し、データとして表示されるのではなく、命令として解釈されてしまいます。攻撃者はモデルに直接アクセスする必要すらありません。
適用対象
出力がそれを解析(パース)する何らかの対象に到達するすべてのエージェントが該当します。例えば、Markdownをレンダリングするチャットインターフェース、提案されたコマンドを実行するコーディングエージェント、生成された引数から構築されるツール呼び出し、2番目のエージェントのプロンプトに投入される要約、あるいはテキストを前方に転送するWebhookなどです。
解決策
フィルターを作成する前に、宛先を列挙します。モデルの出力が到達し、解釈されるすべての場所(HTMLレンダラー、シェル、クエリ、ファイルパス、URLフェッチャー、別のエージェントのコンテキスト、下流のWebhookなど)は、それぞれ異なるルールを持つ個別の境界です。
送信元ではなく、宛先でエンコードします。HTML-escapeはレンダラー向けに行い、クエリ向けにはパラメータ化し、プロセスにはargv配列を渡します。エンコードは解釈が行われる場所で実施すべきです。なぜなら、そこで初めて何が解釈されるのかが分かるからです。
パースし直す必要のある自然文よりも、構造化された出力を優先します。型定義された引数を持つツール呼び出しには検証用のスキーマがありますが、正規表現でファイル名を抽出するような文章にはスキーマがありません。
構文だけでなく、値も検証します。エンコードされたパスであってもパスであることに変わりはありません。ファイルを開く前に、意図したディレクトリ内で解決されるかを確認してください。
外部へのURLは、それ自体を独自の宛先として扱います。レンダリングされた画像やリンクは、クリックしなくてもフェッチを発生させるため、データを外部に持ち出す可能性があります。レンダリングされたリンクが到達できるホストをホワイトリストに登録してください。
境界を唯一のルートにします。宛先エンコーダーを通さずに生のモデル出力を消費できるコードパスが1つでも存在する場合、その制御は形骸化しており、実質的な効果はありません。
コンポーネント
メリット
- 重要なポイントでインジェクションの連鎖を断ち切ります。モデルが完全に誘導されてしまったとしても、下流のシステムがそのテキストを解釈することはないため、下流のシステムを動作させることはできません。
- モデルの挙動に依存しません。モデルが拒否することに依存しないため、モデルのアップグレード、新しいジェイルブレイク(脱獄)、プロンプトの変更を経ても機能し続けます。
- テスト可能です。各宛先にはペイロードと合否(パス/フェイル)が存在するため、この制御は単なる安心感ではなく、確実な証拠をもたらします。
- 早期に適用すれば低コストで済みます。エンコーダーの追加は境界の変更ですが、宛先がいたるところに存在した後に後付けするのは大規模なリファクタリングになります。
リスク
- すべての宛先に対して1つのサニタイザーを使用すること。これは制御が行われているように感じられ、チェックリストを満たしますが、それが想定して書かれた境界以外のすべての境界において誤った処理となります。
- プロダクトを破損させるエンコーディング:過剰なエスケープ処理により、正当なMarkdown、コードブロック、非ラテン文字テキストがノイズに変わってしまいます。そして、それを緩和しようとする圧力は、宛先リストではなくエンコーダーにかかることになります。
- 陳腐化する宛先インベントリ。新しい連携によって消費システムが追加されても、それを見落としたときにエラーが発生することはありません。
- 検知とエンコーディングの混同。不審な文字列がないか出力をスキャンすることは、過去のペイロードを捉えるのには役立ちますが、エンコーディングは攻撃をまったく認識する必要がありません。
非推奨のケース
- 出力が一切解釈されない場合(スコア、列挙型、呼び出し元が比較するブール値など)。代わりに型を制約してください。閉じられた値のセットに対してエンコーダーを適用するのは、形式的な手続きにすぎません。
- レンダリングもプロセス実行も行わず、唯一の消費者がテキストを読む人間だけである、完全にローカルなシングルユーザー向けツールのケース。
- ORMバインディングや、デフォルトでエスケープを行うテンプレートエンジンなど、宛先が構造上すでにパラメータ化されている場合。2つ目のエンコーダーを導入しても何も得られず、二重エンコードが発生する可能性があります。
テクノロジー
事例
- サポートエージェントが、攻撃者のホストを指すMarkdown画像が本文に含まれるチケットを要約します。コンソールがそれをレンダリングし、ブラウザがそのURLをフェッチするため、誰も何もクリックしていないにもかかわらず会話が漏洩します。生のHTMLを無効化し、画像ホストをホワイトリストに登録することで、この問題を解決できます。
- コーディングエージェントがシェルコマンドを提案します。ランナーがその文字列をシェルに渡すため、コマンド区切り文字を含むファイル名が実行されてしまいます。代わりにargv配列を渡すことで、シェルのパースステップを完全に排除できます。
- あるエージェントの要約が、2番目のエージェントのプロンプトに配置されます。要約に指示が含まれており、2番目のエージェントがそれに従いました。この場合、信頼できないスパンをフェンスで囲み、データとしてラベル付けすることが境界となります。
KPI
- 宛先カバー率
- 境界にエンコーダーが設置されている、既知のモデル出力消費システムの割合。100%未満の場合、制御に特定の抜け穴が存在することになります。平均化されたパーセンテージを示すよりも、具体的な宛先を特定する方が有用です。
- ペイロード無害化率
- 境界で無害化された宛先ごとのテストペイロードの割合。目標は100%です。それ以外の数値は、改善すべき数値というよりも、修正すべき宛先を示しています。
- 新規宛先のカバーに要する時間
- 新しい連携が本番稼働してから、そのエンコーダーが用意されるまでの期間。インベントリが一度正しかったかどうかではなく、プロダクトの進化に追従できているかを測定します。
- 境界トリガー数
- 本番環境でエンコーダーが何かを無害化した頻度。完全にゼロである場合は、敵対的な入力が届いていないのではなく、通常はエンコーダーが処理経路上に存在しないことを意味します。
観察された失敗パターン
- レンダリングされたマークアップを介したサイレントな情報漏洩:画像やリンクがクリックなしでフェッチを発生させるため、ユーザーの操作なしに、また誰も気づかないようなエラーも発生せずにデータが外部に流出します。
- 二次インジェクション:コンソール向けに正しくエンコードされた出力が保存され、その後、そのエンコーディングが適用されない別の場所(ログビューア、チケット、ダイジェストメールなど)でレンダリングされます。
- 正常系パスにしか存在しないエンコーダー。エラー分岐、再試行、フォールバックが、何も対策が施されていない別のコードパスを介して同じテキストを出力してしまいます。
- 二重エンコード。2つのレイヤーがそれぞれ正しくエスケープ処理を行い、ユーザーにはプロダクト内でエスケープされた実体参照が表示され、その修正において誤ったレイヤーが削除されてしまいます。
得られた教訓
- フィルターを作成する前に宛先を列挙します。ここでの実際の失敗のほぼすべては、エンコーダーの書き間違いではなく、誰もリストアップしていなかった消費システムが存在することに起因します。
- 忘れがちな宛先が画面であることは稀です。それはWebhook、ログビューア、エクスポート、またはダイジェストメールなど、レンダリングであると誰も意識しないまま出力が送られる場所です。
- エンコーディングは検知よりも優れています。なぜなら、エンコーディングは攻撃を認識する必要がないからです。ペイロードのブラックリストは、すでに公開されている攻撃の記述にすぎません。
- 単一の `sanitise()` を解決策にしないでください。その名前は完全性を連想させますが、その挙動は正確には1つの宛先に対してのみ正しいものです。
FAQ
- これはプロンプトインジェクション対策として入力をフィルタリングすることと同じではないのですか?
- いいえ、これらは相反する両端を防御するものです。入力フィルタリングはモデルが誘導されるのを防ごうとするもので、モデルに依存します。一方、出力境界エンコーディングは、誘導がすでに成功したと仮定した上で、出力が実行されるのを防ぐものであり、モデルには一切依存しません。前者のみを備えたシステムは、新しいジェイルブレイク(脱獄)が現れた瞬間に破綻します。
- これは単なる出力サニタイズではないのですか?なぜすべてに対して1つのサニタイザーではいけないのですか?
- エンコーディングは文脈に依存するからです。引用符のエスケープはHTMLレンダラーを保護しますが、シェルに対しては何の効果もありません。シェルクォーティングはシェルを保護しますが、表示されるテキストを破損させます。共有関数は1つの文脈を選択せざるを得ず、他の文脈では誤った処理となり、カバーできているように見えて単一障害点になってしまいます。
- モデルは自社のものであり、プロンプトも固定されています。それでもこれが必要ですか?
- はい。モデルが読み込むコンテンツが、取得したページ、ユーザーファイル、ツールの実行結果、メモリレコードなど、外部から提供されるものである場合は該当します。指示はプロンプトから直接与えられる必要はありません。モデルが読み込むあらゆるコンテンツを介して指示が入り込むため、プロンプトが固定されているからといって、それを防げるわけではありません。