LLMアプリのセキュリティ — インジェクションから出力処理まで
データが指示に変わる瞬間 — 直接・間接プロンプトインジェクション
一行まとめ
モデルは、入力のうちどの文が命令でどの文が資料なのかを確実に区別できません。そのため「何をしてよいか」は、モデルではなくモデルの外にあるサーバーのポリシーが決めなければなりません。
なぜこれが必要だったのか
SQLインジェクションを思い浮かべるとわかりやすいです。コードとデータが1つの文字列に混ざっていたためにデータがコードとして実行され、そこで私たちはプレースホルダー(パラメーター化)によって両者の間にエンジンが守る境界を作りました。LLMにはそのような境界がありません。システム文・ユーザーの質問・検索したドキュメント・ツールの結果は、結局1列につなげたトークン列にすぎず、「ここからは資料」という目印はモデルが学習で身につけた傾向にすぎないため、強制されるルールではありません。
ユーザーが自分の質問に指示を混ぜるのが直接インジェクションです。さらに厄介なのは間接インジェクションです。アプリが検索してきたドキュメント、要約するよう渡されたWebページ・メール、ツールが返した結果の中に指示のような文が隠れていれば、攻撃者は私たちのユーザーに話しかける必要すらなく、ドキュメントを1つ仕込んでおくだけで済みます。RAGとツールがつながったアプリほど、この経路は広がります。OWASP LLM Top 10 2025はこれを1番目のリスク(LLM01 プロンプトインジェクション)に位置づけています。
どのように動作するのか
このコースのラボでは、外部のモデルの代わりにルールで動く偽のモデルを使います。同じ入力なら常に同じ答えを返さないと、採点ツールが「攻撃を送ったら何が起きたか」を何度でも同じ結果で測れないからです。このモデルは、本物のモデルの弱点を誇張したカリカチュアです。
| 偽のモデルの性質 | 本物のモデルで対応するもの |
|---|---|
| 入力のどこにあっても指示マーカーに従う | 役割の区別は学習された傾向にすぎないため、資料の中の指示に従うことがある |
| 全角・空白・大文字小文字・base64を同じ指示として読む | 言い換えた指示、別の言語、エンコードされた指示も理解する |
| ツール呼び出しをテキストの行として出す | 関数呼び出しも、結局はアプリが解釈する出力である |
正直に述べておくべき限界があります。本物のモデルは確率的です。同じ攻撃が10回に1回だけ通ることもあれば、表現を変えると通ることもあります。偽のモデルに対して「マーカーを除去するフィルター」が効いたとしても、それはフィルターがこのモデルの文法をすべて把握していたというだけで、本物のモデルに対して同じフィルターが防げるという証拠にはなりません。テストに何度か通ったからといって安全が証明されない理由がこれです。
そのため、防御の中心はモデルの外に置きます。
- 最小権限 — モデルが出した行動をそのまま実行しません。宛先・対象・引数は、リクエストしたユーザーの権限に基づいてサーバーが決めます。
- 人による確認 — 信頼できない文が混じった回答から出た高リスクの行動は保留し、リクエストした本人が確認したときに一度だけ実行します。
- 出典はサーバーが付ける — モデルが書いた引用ではなく、サーバーが実際に入力に入れたドキュメントとその信頼レベルを示します。
- フィルター・区切り表示は補助層 — 攻撃の量を減らし検知に使いますが、境界としては信頼しません。
これらの層がなぜモデルの「外」になければならないのかは、失敗したときを思い浮かべるとはっきりします。フィルターが漏れればモデルは指示に従い、その瞬間に最後に残るのはサーバーのルールです。逆に、サーバーのルールが漏れるのはコードの欠陥なのでテストで見つけることができ、一度直せば再び漏れることはありません。確率的な部品の前には決定的な部品を置く — このコース全体を貫く原則がこれです。
間接インジェクションの入り口は、思ったより多くあります。検索ドキュメントだけでなく、ユーザーが貼り付けたメール、過去の会話で保存しておいたメモ、Webページを読み込んだツールの結果、別のエージェントが渡した要約まで、すべてモデルの入力になります。アプリに「記憶」や「ツール」を1つ付けるたびに、他人が書いた文が入ってくる扉が1つ増えると考えてください。
現場で見かける姿
顧客がアップロードしたドキュメントを要約する社内アシスタントが、ドキュメント内の文のとおりにメールを送ってしまうインシデントや、Webページ要約ツールがページ内の指示どおりに会話内容を外部のアドレスへ送ってしまうインシデントが代表的です。ログで見ると、形はいつも同じです。あるリクエストのプロンプトに外部から来た文が入り、まさにそのリクエストで誰も指示していない行動が起きています。そのため本番運用では、プロンプトに何がどこから入ったか(リクエストid・断片・出典)を残しておくことが調査の出発点になります。
調査はいつも副作用から逆にたどります。送信されたメール1通からリクエストidへ、リクエストidからそのリクエストの入力断片へ、断片からその文の出典(ドキュメント・メモ・質問・設定)へ。記録に断片と出典がなければこの連鎖が途切れ、「モデルがおかしな振る舞いをした」で止まってしまいます。そのため、プロンプトの記録はデバッグ用のダンプではなく監査資料として設計する必要があります — 次のモジュールで見るように、機密情報はマスクした状態で。
だからといって、フィルターと区切り表示を捨てろということではありません。外部の文を「以下は資料である」のような表示で囲み、指示のような文に印を付けておけば、本物のモデルがそれに従う確率はたいてい下がります。ただし、下がることと防げることは別物です。補助層は検知と警報に使い、行動の許可は常にサーバーのルールが行います。
次のラボで行うこと
社内ドキュメントアシスタントを展開し、モデルが実際に受け取った入力を記録から読み、ドキュメント群から形の異なる隠れた指示をすべて見つけ、攻撃を一度再現します。その後、宛先ルール・保留と確認・サーバーが付ける出典を順に組み込み、最後に3つの部署で同じ症状(社外に送られたメール)が互いに異なる経路で起きたインシデントを切り分けます。