コーディングエージェントのプロンプトインジェクション対策
攻撃者が書き込めるテキストは、サーバー上のコーディングエージェントに命令として届きます。実際の侵入経路を整理し、被害を減らす対策を優先順位付きで解説します。
コーディングエージェントに対するプロンプトインジェクションとは
コーディングエージェントに対するプロンプトインジェクションは、簡単に説明できます。エージェントが読むテキストが、エージェントが従う命令として扱われます。エージェントは、ファイル、pull request のコメント、Web ページ、またはツール呼び出しの結果を開きます。これらはすべて、ユーザー自身のリクエストと同じ種類のテキストとして届きます。攻撃者がそのテキストのいずれかを制御できる場合、攻撃者はユーザーのセッションに書き込んでいることになります。
現在のすべてのエージェント製品に、この性質があります。モデルは、1 つのトークン列を受け取ります。ユーザーのリクエスト、system prompt、ファイルの内容、ツールの結果が連結され、モデルは次に続く内容を予測します。トークンに権限を示すビットはありません。どの部分をユーザーが承認したのか、どの部分が第三者の README ファイルに由来するのかを、形式から判別することはできません。
このページでは脅威モデルを扱います。攻撃者が制御するテキストがサーバー上で実行されるエージェントに到達する経路、その各時点で攻撃者が得るもの、そして対策にかける価値がある労力を説明します。他のガイドで説明している封じ込めの方法は、エージェントから何を封じ込めるのかを理解して初めて意味を持ちます。
モデルがコンテンツと指示を分離できない理由
学習は有効ですが、問題を解決するものではありません。現在のモデルは、取得したテキストを疑って扱うように訓練されており、単純な試みの多くを拒否します。ただし、拒否は確率的な反応であり、規則ではありません。攻撃者は表現を変えて再試行したり、想定されていない形式にテキストを隠したりできます。また、試行できる表現の数にも制限はありません。
OWASP GenAI project では、この問題を LLM01:2025 Prompt Injection として扱い、2 種類に分けています。直接インジェクションは、ユーザー自身のプロンプトによってモデルの動作が変わるものです。間接インジェクションは、Web サイトやファイルなどの外部コンテンツをモデルが処理した際に、そのコンテンツによって動作が変わるものです。サーバーで重要なのは間接インジェクションです。エージェントは、ユーザーが入力するテキストよりもはるかに多くのテキストを読み取るためです。
最初の体系的な研究は、Greshake 氏らによる Not what you've signed up for (2023) です。ここで覚えておくべき結論は、次の一文です。アプリケーションが取得したテキストを、ツールを呼び出せるモデルに入力すると、そのテキストの処理は任意コード実行に近いものになります。
読み取りが侵害につながる条件
悪意のあるテキストを読むだけでは、それ自体は被害になりません。被害が発生するには、マシンの外部へ出る経路が必要です。
Simon Willison は、2025 年 6 月にこの組み合わせを致命的な三要素と名付けました。機密データを保持し、信頼できないコンテンツにさらされ、外部へデータを送信できるエージェントは、最初の要素を3番目の経路から外部へ移すよう誘導される可能性があります。
VPS 上のコーディングエージェントは、初日からこれらすべてを備えています。機密データは、ソースコード、.env ファイル、SSH キー、シェル履歴です。信頼できないコンテンツは、読み取るすべてのリポジトリ、ページ、ツールの結果です。外部へ出る経路は、git push、curl、npm publish、プルリクエストの本文、またはターミナルに表示されてクリックするリンクです。
2番目の条件は取り除けません。信頼できないテキストを読むことが、エージェントに任せた仕事だからです。したがって、実用的な防御はすべて、残りの2つの条件に対して行います。
サーバー上のコーディングエージェントに信頼できないテキストが到達する場所
エージェントが作業するリポジトリ
チェックアウト内のすべてのファイルが入力です。ソースコードのコメント、README.md、変更履歴、テスト用フィクスチャ、ベンダーコード、エージェント用の指示ファイル自体が該当します。CLAUDE.md、AGENTS.mdおよびそれらに相当するファイルも含まれます。コードベースの理解を求められたエージェントは、これらを読みます。それが依頼した処理だからです。
攻撃者は、リポジトリをクローンしてエージェントを向けるすべての人に影響を及ぼせます。指示ファイルは、指示として読まれることを目的としているため、最も直接的な経路です。CLAUDE.mdに役立つ4行と、エージェントを別の動作へ誘導する1行を追加したプルリクエストは、人間のレビュアーが流し読みして見逃す変更になり得ます。
Issue、プルリクエスト、コードレビューのコメント
トラッカーに第三者が入力できる内容は、トリアージを依頼した時点でエージェントに届きます。2025年5月、Invariant Labsはこの構造に該当するGitHub MCPの調査結果を公開しました。ある開発者のエージェントは、1つの公開リポジトリと複数の非公開リポジトリにアクセスできました。攻撃者は公開リポジトリにIssueを登録しました。開発者がエージェントにオープンなIssueの確認を依頼すると、エージェントは非公開リポジトリの内容を読み取り、公開側のプルリクエストに書き込みました。
この報告が示すのは、MCPサーバーのコード不具合ではなく、アーキテクチャ上の問題です。エージェントは広範な権限を持つ1つのアクセストークンを使用し、公開された受信箱から読み取り、書き込みも許可されていました。通常の意味で設定を誤っていたわけではありません。そのため必要なのはパッチではなく、権限のスコープ設定です。
エージェントが取得する Web ページ
ドキュメント、フォーラムの回答、ベンダーのページ、検索結果などです。いずれにも、利用者ではなくエージェントに向けて書かれたテキストが含まれる可能性があります。HTMLをテキストに変換すると、ブラウザーに表示されない内容もモデルに届くため、攻撃者が利用できる余地が広がります。
攻撃者は、エージェントが最も監視されていない時点で制御を得ます。エージェントが検索中に取得したページの全文を読む人はいません。
MCPツールの出力
MCP(model context protocol)は、エージェントを外部ツールに接続する一般的な方法です。結果はテキストとして返され、そのままコンテキストウィンドウに入ります。ここには1つではなく2つの攻撃面があります。ツールが返すデータが1つ目です。モデルが呼び出し時期を判断するために読む、ツール自体の名前と説明がもう1つです。管理していないサーバーは、呼び出しの間にどちらも変更できます。
攻撃者が1つのツールの出力にテキストを入り込ませると、エージェントが利用する他のすべてのツールに到達します。これにより、価値の低い処理へのインジェクションが、価値の高い処理を実行させることがあります。
CIログ、ビルド出力、依存関係のメタデータ
npm installは、自分で作成していないパッケージからのテキストを出力します。テストが失敗すると、ライブラリ由来のアサーションメッセージが出力されます。継続的インテグレーション(CI)のジョブログには、サードパーティーの出力が数千行含まれます。エージェントに失敗したビルドの修正を依頼すると、エージェントはそのすべてを読み取ります。
この場合、攻撃者が得るのはビルドマシンへの足掛かりです。ビルドマシンには通常、デプロイ用の認証情報やレジストリトークンが保存されています。一方で、ノートPCほど注意深く監視されていないことが多くあります。
攻撃者が実際に取得できるもの
想定しておくべき結果は4つあります。
認証情報の窃取。 エージェントプロセスが読み取れるものはすべて対象になります。環境変数、~/.aws/credentials、~/.ssh、ghトークン、Docker設定ファイルなどです。これらの外部送信に curl は必要ありません。ブランチへのコミット、プルリクエストの説明、レジストリへのパッケージ公開、攻撃者が管理する名前へのDNS問い合わせによっても、データはホストの外部へ移動します。
承認してしまうコード変更。 エージェントの役割はコードを書くことです。そのため、わずかに誤った行を書かせるのが最も容易な結果です。依存関係の追加や、トークンを別の場所へ送信するログに含めるロギング呼び出しなどが該当します。
永続化。 一度書き込まれたファイルは、モデルが動作していなくても機能し続けます。.git/hooks のフック、package.json 内の postinstall スクリプト、シェルの起動ファイルに追加された行、CLAUDE.md の追加行などです。次のコマンドがそれを実行します。
ネットワーク内部での横移動。 エージェントは配置した場所で実行されます。そのホストから loopback 上のデータベース、内部管理サービス、クラウドプロバイダーのメタデータサービス、またはプライベートネットワーク上の別ホストへ接続できる場合、エージェントを操作するものも同じ接続を利用できます。
自動承認モードでは最後の確認がなくなります
デフォルトモードでは、Claude Code はコマンドの実行やファイルの編集前に確認を求めます。この確認が、上記の各攻撃面と実際の操作の間にある人によるチェックです。確認を省略するモードでは、このチェックもなくなります。
ドキュメントでは bypassPermissions について明確に説明されています。Claude Code が損害を与えられないコンテナや VM など、隔離された環境でのみ使用してください。auto モードはこれより制限が緩く、バックグラウンドの安全性チェックで操作が要求に沿っていることを確認しながら、ツール呼び出しを自動承認します。これらのチェックで多くの問題を検出できます。ただし、これはモデルの出力に対するモデルの判断です。そのため、境界ではなくフィルターとして扱ってください。
管理者は両方の設定を無効にできます。設定ファイルで permissions.disableBypassPermissionsMode または permissions.disableAutoMode を "disable" に設定し、そのファイルを managed settings に配置してください。これにより、チェックアウトしたプロジェクトで設定を上書きできなくなります。各ルールが適用される場所については、Claude Code の自動モードと権限ルールのガイドで説明しています。
防御策:得られる効果が大きい順
どれも根本的な修正ではありません。それぞれ、エージェントが保持するものを減らすか、保持したものを使って実行できる操作を制限します。
- 破棄して再構築できるマシン。侵害されても、インシデント対応ではなく 1 時間の再構築で済みます。
- 自分の認証情報とは分離し、1 つのリポジトリだけを対象にして、有効期間を短くした認証情報。
- エージェントが実行するコマンドに継承される環境に、長期間有効な Secret を置かないこと。
- エージェントが起動するすべてのプロセスに適用される、ネットワークの外向き通信とファイルアクセスに対する OS の強制制御。
- 書き込みとネットワーク呼び出しに対する承認プロンプトを有効にしておくこと。
- 名前を付けて制御できる操作に対する、決定論的な最後の防波堤としての Hook。
- マージする前に Diff を確認すること。
順序には意味があります。1 から 4 は、モデルが攻撃者に完全に制御されていても機能します。5 から 7 は人間が注意を払うことに依存します。長時間のエージェント実行では、まさにその注意が失われます。
破棄できるマシンにエージェントを配置する
Checkout と 1 つの対象限定 Token だけを保持する VPS は、自分の鍵を保存した Laptop よりも攻撃者にとってはるかに小さな獲物です。エージェントは専用の非特権ユーザーとして実行します。自分の Login Account や root では実行しません。コーディングエージェント用の破棄可能な VM と VPS で最小権限ユーザーを使用する方法 では構築方法を説明し、VPS で Claude Code を安全に実行する方法 では日常的な運用方法を説明しています。
環境から Secret を取り除く
環境変数はすべての子プロセスから読み取れます。つまり、エージェントが実行するすべてのコマンドから読み取れます。Claude Code の Sandbox では、Sandbox 内でコマンドを実行する前に、指定した変数を解除できます。Linux では、最初に 2 つの Package が必要です。
sudo apt-get install bubblewrap socat次に ~/.claude/settings.json で設定します。
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["github.com", "*.npmjs.org"]
},
"credentials": {
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" },
{ "name": "NPM_TOKEN", "mode": "deny" }
]
}
}
}deny の設定は、Sandbox 内でコマンドを実行する前にその変数を解除します。allowedDomains には、列挙した Host への Sandbox 内のコマンドを適用します。credentials の Block を使用するには Claude Code v2.1.187 以降が必要です。これは August 2026 に確認した内容です。Session 内で /sandbox を実行すると、有効な Layer と不足している Dependency を確認できます。その Box にそもそもどの Secret を置く必要があるかを判断することが、作業の大部分を占めます。AI エージェントの到達範囲から Secret を外す方法 では、この点を詳しく扱っています。
OS でネットワークの外向き通信を制限する
Firewall Rule は、モデルが何を判断したかに関係なく適用されます。エージェントを専用の agent ユーザーとして実行し、そのユーザーから送信される通信を遮断します。
table inet agentcage {
chain output {
type filter hook output priority filter; policy accept;
meta skuid "agent" ct state established,related accept
meta skuid "agent" oif lo accept
meta skuid "agent" counter drop
}
}これにより agent ユーザーは Loopback だけを使用できるため、通信は同じ Box 上で実行する Proxy を経由する必要があります。Hostname の Allowlist は Proxy に保持します。https_proxy でその Proxy を指定すると、Client は CONNECT Request を送信し、Name Lookup は Proxy が実行します。そのため、エージェント自身に外向きの DNS(Domain Name System)は必要ありません。sudo nft list ruleset で設定を確認し、エージェントが新しい接続先へ到達しようとしたときに Drop Rule の Counter が増えることを確認します。
Firewall の変更を適用する間は、2 つ目の SSH Session を開いたままにします。Container Runtime がこれらの Rule にどのような処理を行うかも確認してください。Docker は独自の Chain を書き込みます。公開した Docker Port が ufw を迂回する仕組み では、その原因となる想定外の動作を説明しています。
Hook:モデルが言い逃れできない Check
Permission Rule と Hook はモデルではなく Claude Code が適用します。ドキュメントにも明記されています。Prompt または CLAUDE.md の指示は Claude が実行しようとする操作を方向付けますが、Claude Code が許可する操作を変更することはありません。この違いに価値があります。CLAUDE.md に「curl を実行しないこと」と書いても、Injected Paragraph が反論できる提案にすぎません。Hook は Exit Code を返す Process です。
.claude/settings.json に PreToolUse Hook を登録します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/no-egress.sh"
}
]
}
]
}
}Hook は Tool Call を JSON として Standard Input から受け取ります。Exit Code 2 は Call を遮断し、Standard Error に出力した理由を Claude に表示します。Exit Code 0 は、通常の Permission Flow に従って Call を続行します。
#!/usr/bin/env bash
# PreToolUse: stdin holds the tool call, exit 2 blocks it.
cmd=$(jq -r '.tool_input.command // ""')
if printf '%s' "$cmd" | grep -qE '(^|[;&|]|\s)(curl|wget|nc|ncat)(\s|$)'; then
echo "Blocked: this repository does not allow outbound network commands." >&2
exit 2
fi
exit 0ここで重要な点を確認します。これは Shell String に対する Denylist であり、Shell String に対する Denylist には漏れがあります。python3 -c は curl という単語を使わずに Socket を開けます。make deploy の Target を使えば、同じ Call をさらに 1 段階隠せます。名前を付けられる誤操作には Hook を設定し、実際に依存する境界は Kernel または Network に配置してください。
Permission の Deny Rule には、知っておくべき照合範囲の制限もあります。Read と Edit の Deny Rule は、Claude 独自の File Tool と、Bash で Claude が認識する File Command を対象にします。対象となるのは cat、head、tail、sed などです。Python または Node の Script が自分で File を開く場合は対象になりません。Rule は最初に Deny、次に Ask、最後に Allow の順で評価されます。そのため、Deny Rule に Allowlist の例外を持たせることはできません。
{
"permissions": {
"deny": [
"Read(.env)",
"Read(./secrets/**)",
"Bash(git push *)"
]
}
}外部へ送信される内容を監視し、Diff を確認する
エージェントの実行では、Diff と複数の Network Call が生成されます。いずれも Merge または Deploy の前に確認してください。Self-hosted の Security Review を実行する方法 で Diff を確認すると、人間が軽く目を通すだけでは見つけにくい種類の変更を検出できます。また、コーディングエージェントが Box の外部へ送信する内容を把握する方法 により通常の Traffic が分かるため、異常な Request に気付きやすくなります。
未解決の課題
現在、コンテンツと命令を確実に分離する仕組みはありません。提供されている防御策は、いずれも誤検知や見逃しが発生するフィルターか、被害を抑えるための制限です。テキストの一部を、決して従ってはならないデータとしてスタック内で明示する仕組みはありません。
フィルターは役立ちますが、失敗もします。ほとんどのプロンプトインジェクションを検出できる分類器でも、毎回正しく判定しなければなりません。一方、攻撃者は1回成功すれば十分です。この非対称性があるため、防御策の公表成功率は保証ではなく、次の攻撃を試す際の出発点になります。
最も有望な研究は、モデルレベルではなく設計レベルで進んでいます。Defeating Prompt Injections by Design の CaMeL(Debenedetti ら、2025)は、まず信頼できるリクエストから制御フローとデータフローを抽出します。これにより、信頼できないデータがプログラムの動作を変更できないようにします。そのうえで、ツール呼び出し時に機能の権限チェックを適用します。このコストは、AgentDojo ベンチマークに関する論文自身の数値に示されています。
The data behind this chart
[
{
"label": "Undefended agent",
"tasks_solved_pct": 84
},
{
"label": "CaMeL",
"tasks_solved_pct": 77
}
]防御されていないエージェントはタスクの 84 パーセントを解決しました。CaMeL は、セキュリティ保証付きで 77 パーセントを解決しました。これらは1つのベンチマークにおける論文の公表値であり、実際のワークロードを測定したものではありません。両者の差は、現在の現実的な保証にかかるコストのおおよその大きさです。
このような設計が、日常的に使用するツールへ組み込まれるまでは、エージェントがいずれ侵害されることを前提にし、その事態を問題にならないものにしてください。使い捨てのマシン、範囲を限定した認証情報、制御された外向き通信、そして差分を読む習慣が必要なのは、そのためです。
FAQ
ファイル内の指示を無視するようエージェントに伝えれば、プロンプトインジェクションを防げますか?
いいえ。その文も攻撃と同じコンテキストウィンドウ内のテキストであり、攻撃者のテキストと同じ条件で競合します。Claude Code のドキュメントは境界を明確に示しています。プロンプト内の指示や CLAUDE.md はエージェントが実行しようとする内容を決めますが、ツールで許可される操作は変更しません。指示ファイルは意図を示す文書として扱い、依存する内容は権限ルール、PreToolUse hook、またはファイアウォールルールに設定してください。
エージェントが自分のリポジトリだけを操作する場合でも、プロンプトインジェクションは現実的なリスクですか?
はい。リポジトリには、自分で書いていないテキストが大量に含まれているためです。依存関係の README ファイル、lockfile の URL、テスト用フィクスチャ、vendor code、npm install の出力は、通常のタスク中に取り込まれます。issue tracker やドキュメントサイトから取得した内容も同じです。エージェントが読む量が増えるほどリスクも高まります。実用的なエージェントは大量の情報を読み取ります。
エージェントをコンテナ内で実行すれば解決しますか?
被害は抑えられます。ただし、認証情報も取り除いた場合に限ります。SSH agent を転送し、環境変数に cloud credentials を設定し、ネットワークアクセスを制限していないコンテナでは、攻撃者にホスト上のほぼすべてを渡すことになります。コンテナによって得られる本当の利点は、削除できるファイルシステムと、egress ルールを適用できるクリーンな環境です。1 つのリポジトリに限定した token と組み合わせてください。
リスクを最も大きく減らす単一の変更は何ですか?
まず、エージェントのコマンドが継承する環境から長期間有効な認証情報を削除してください。次に、そのマシンにデフォルト拒否の egress ポリシーを設定してください。この 2 つを組み合わせると、致命的な三要素の 3 番目の条件を断ち切れます。テキストによってエージェントが乗っ取られる可能性は残りますが、エージェントが到達できるデータを有効な送信先へ送れなくなります。承認プロンプトと diff の確認も有効です。ただし、長時間の実行中も人間が注意を維持する必要があります。そのため、これら 2 つの変更より優先度は下がります。