生成AIを本番のプロダクトに載せる企業が一気に増えました。しかし「モデルが賢いか」と「そのアプリが安全か」は、まったく別の問題です。本番のLLMアプリのセキュリティは、プロンプトを工夫することでは解決しません。 結論から言えば、次の3つのアーキテクチャ原則にほぼ集約されます。
- LLMの出力を信頼しない(ゼロトラスト) — 出力は「別のユーザー入力」として、下流に渡す前に必ず検証する。
- ツール(外部連携)の権限を最小化する — エージェントに破壊的な実行権限そのものを渡さない。
- 高影響の操作は人間が承認する — 返金・削除・送金・権限変更などは、決定的なコードと人間のゲートを通す。
この記事は、生成AIパイプラインを本番で運用し、かつアプリケーションセキュリティ(Next.js × Supabase のアプリ防御や脅威モデリング)を専門にしてきた立場から、OWASP Top 10 for LLM Applications(2025年版) と NIST AI RMF に準拠して、TypeScript / Zod / Vercel AI SDK の実コードで「本番で効く」対策だけを示します。抽象論ではなく、明日のPRに入れられる設計です。
⚠️ 本記事の攻撃例は、防御設計を理解するための概念説明に限定し、悪用可能な完全なペイロードは記載しません。検証は必ず自分が管理する環境で行ってください。
なぜLLMアプリのセキュリティは「従来と違う」のか
従来のWebアプリのセキュリティは、「信頼できない入力(ユーザー)」と「信頼できるコード(自分たち)」の間に明確な境界を引き、その境界で検証・エスケープする——という考え方で成立してきました。
LLMアプリでは、この前提が3つ崩れます。
- 決定論的でない。 同じ入力でも出力が変わりうる。「このパターンだけ弾く」という完全なホワイトリストが作れない。
- 出力を信頼できない。 LLMの出力は、実質的にユーザーが(プロンプト経由で)間接的に操作できる。LLMの出力を信頼して下流に流すのは、ユーザー入力を無検証でSQLに渡すのと同じです。
- 信頼境界が曖昧になる。 RAGで外部文書を、エージェントでツールの実行結果を、プロンプトに次々と流し込む。どこまでが「信頼できるコンテキスト」なのかが設計しないと決まらない。
だから防御は「モデルをどう賢くするか」ではなく、「モデルを確率的で信頼できない一部品として扱い、その周りのアーキテクチャで安全を担保する」方向に置きます。これが本記事を貫く一本の軸です。
OWASP LLM Top 10(2025)早見表
まず全体像です。各項目の詳細と防御コードはこの後で扱います。
| ID | 項目(2025) | 一言でいうと | 主要な防御 |
|---|---|---|---|
| LLM01 | Prompt Injection | 入力でモデルの挙動を乗っ取る | 出力ゼロトラスト+ツール最小権限+人間承認 |
| LLM02 | Sensitive Information Disclosure | 出力から機密・PIIが漏れる | 最小権限アクセス、入出力のサニタイズ |
| LLM03 | Supply Chain | モデル/データ/依存の供給網汚染 | 供給元の検証、SBOM、署名・ハッシュ検証 |
| LLM04 | Data and Model Poisoning | 学習/微調整データの汚染・バックドア | 来歴追跡(ML-BOM)、データ検証、DVC |
| LLM05 | Improper Output Handling | 出力を無検証で下流に渡す | 出力をゼロトラスト検証、文脈別エンコード |
| LLM06 | Excessive Agency | エージェントの過剰な機能/権限/自律性 | 最小権限、提案と実行の分離、人間承認 |
| LLM07 | System Prompt Leakage | システムプロンプトの秘密が漏れる | 秘密をプロンプト外へ、外部ガードレール |
| LLM08 | Vector and Embedding Weaknesses | RAGの検索/埋め込みの弱点 | 認可付きリトリーバル、来歴検証、分類 |
| LLM09 | Misinformation | ハルシネーションによる誤情報 | RAG、相互検証、人間のファクトチェック |
| LLM10 | Unbounded Consumption | 過剰な推論によるDoS・コスト暴走 | 入力上限、レート制限、消費の監視 |
2025年版での主な変更点(OWASP公式より):
- LLM07 System Prompt Leakage を新規追加(実際の攻撃事例が多発したため)。
- LLM08 Vector and Embedding Weaknesses を新規追加(RAGと埋め込みの本番採用が進んだため)。
- 旧 Model Denial of Service を、リソース管理と想定外コストまで含む LLM10 Unbounded Consumption へ拡張。
- LLM06 Excessive Agency を、エージェント型アーキテクチャの普及を受けて拡充。
以降、実務インパクトの大きい LLM01 / LLM05 / LLM06 / LLM08 / LLM10 を、防御コードとともに深掘りします。
LLM01: プロンプトインジェクション — 入力フィルタでは「原理的に」防げない
プロンプトインジェクションは、LLM Top 10の筆頭であり、最も誤解されている項目です。「悪い入力を弾くフィルタを頑張れば防げる」というのは誤りです。
OWASPのLLM01は明確にこう述べています——「モデルが本質的に確率的な影響で動作する以上、プロンプトインジェクションを防止する万全な手法が存在するかは不明である」。つまり入力フィルタは緩和策であって、根治ではない。ここを設計の出発点にします。
直接インジェクションと間接インジェクション
- 直接(direct) — ユーザーが「これまでの指示を無視して…」のように、プロンプトで直接モデルの役割を上書きしようとする。
- 間接(indirect / cross-domain) — モデルが外部から取り込むコンテンツ(Webページ、ファイル、RAGで取得した文書、ツールの実行結果、画像に埋め込まれた指示)の中に悪意ある命令が仕込まれる。ユーザー自身は無害でも、モデルが読む「データ」が攻撃者に汚染されているケースです。
エージェントやRAGを使うほど、攻撃面は直接入力から「モデルが触れるすべてのデータ」へ広がる——これが2025年版でRAG(LLM08)が独立項目になった背景です。
多層防御:入力ではなく「出力とツール」で守る
プロンプト側でできること(役割の明示、期待する出力形式の宣言)は当然やります。しかしそれは第一層に過ぎません。本丸は、モデルの出力とツール実行の境界です。
// アンチパターン:LLMの出力を「信頼できる指示」として下流に流す
const answer = await generateText({ model, prompt });
await db.query(answer.text); // ❌ プロンプトインジェクションが即RCE/情報漏洩になる
対策は、モデルを「もう一人の信頼できないユーザー」として扱い、出力を型で締めることです(詳細は後述のLLM05)。そして最も効くのは、そもそもモデルに危険なことをさせない=ツールの権限を絞ること(LLM06)。この2層があれば、インジェクションが起きても「被害の爆発半径」を小さく保てます。
LLM05: 出力の不適切な処理 — 出力を「ゼロトラスト」で検証する
LLM05(Improper Output Handling)は、LLMの出力を検証せずに他コンポーネント(DB・シェル・HTML・別API)へ渡すことで起きます。OWASPの言葉を借りれば「モデルを他のユーザーと同様に扱い、ゼロトラストで接する」。
Vercel AI SDK の generateObject と Zod を使えば、出力の「型と値域」を境界で強制できます。ここでのポイントは、スキーマ検証を通っても、業務ルールは決定的コードで再確認することです。
import { generateObject } from "ai";
import { z } from "zod";
// LLM出力は「別のユーザー入力」= ゼロトラスト。スキーマで境界を締める。
const RefundDecision = z.object({
action: z.enum(["approve", "reject", "escalate"]),
amountJpy: z.number().int().min(0).max(50_000), // 上限を「型」で強制する
reason: z.string().max(200),
});
const { object } = await generateObject({
model: "anthropic/claude-opus-4-8",
schema: RefundDecision,
prompt,
});
// ここまで来た object は「形式的に」検証済み。だが業務ルールは別問題。
// プロンプトインジェクションで action=approve に誘導されても、
// 決定的なコードが最終防衛線になる。
if (object.action === "approve" && object.amountJpy > refundableBalance(order)) {
throw new PolicyError("LLMが残高を超える返金を提案しました");
}
出力をSQL・HTML・シェルに渡すなら、文脈別のエンコード(HTMLエスケープ、パラメータ化クエリ)を必ず挟みます。これは従来のインジェクション対策とまったく同じで、**「LLMの出力は攻撃者が操作できるかもしれない文字列」**として扱えば自然に導けます。構造化出力の信頼性を上げる設計は structured output の信頼性設計 で詳しく扱っています。
LLM06: 過剰なエージェンシー — 「提案」と「実行」を分離する
エージェントに「返金する」「ファイルを消す」「メールを送る」ツールを渡した瞬間、プロンプトインジェクションは実際の被害に変わります。OWASPはこれをExcessive Agencyと呼び、原因を過剰な機能・過剰な権限・過剰な自律性の3つに分解しています。
推奨される防御は明快です——(1) ツールと機能を必要最小限に絞る、(2) 最小権限、(3) 高影響アクションは human-in-the-loop(人間承認)。
設計の要は「エージェントに実行権限を渡さない」こと。エージェントには「提案」までをさせ、実際の破壊的操作はコード側で、決定的な業務ルールと人間承認を通して行います。
import { tool } from "ai";
import { z } from "zod";
// 破壊的アクションは「実行」せず「提案」を返すツールにする。
// ツール自体は最小権限(読み取り+提案レコード作成のみ)。
const proposeRefund = tool({
description: "返金の提案を作成する(実行はしない)",
inputSchema: z.object({
orderId: z.string().uuid(),
amountJpy: z.number().int().positive(),
}),
execute: async ({ orderId, amountJpy }) => {
// 副作用は「提案レコードの作成」だけ。ここで実際の送金はしない。
const proposalId = await createRefundProposal({ orderId, amountJpy });
return { proposalId };
},
});
// 高影響アクションは human-in-the-loop(OWASP LLM06 の推奨)。
// 実行は決定的コード側で、業務ルール検証 + 冪等キー付きで行う。
async function executeRefund(proposalId: string, approver: Admin) {
const p = await getProposal(proposalId);
assertWithinPolicy(p, approver); // 残高・上限・権限を決定的に再検証
// 冪等キーで「二重返金」を構造的に防ぐ(分散システムの基本)
await refundIdempotent(p.orderId, p.amountJpy, { idempotencyKey: p.proposalId });
}
補足すると、この「提案と実行の分離+冪等キー」は新しい発想ではありません。私が決済基盤で本番の二重課金を0件に保つために使ってきた信頼性・冪等性の設計そのものです。LLMエージェント時代のセキュリティは、分散システムの信頼性設計とほぼ地続き——確率的で信頼できないコンポーネント(=LLM)を、決定的なガードで囲う。エージェントの設計全般は AIエージェントのツール実行設計 を参照してください。
LLM08 / LLM02: RAGの情報漏洩 — 「検索」に認可を埋め込む
RAG(検索拡張生成)は、誤情報(LLM09)を減らす有効な手段ですが、それ自体が2つの新しい攻撃面を持ち込みます。
- 間接プロンプトインジェクション(LLM08) — 取得した文書に悪意ある指示が仕込まれている。
- 認可の甘い検索による情報漏洩(LLM02) — ベクトル検索に権限フィルタが無く、他テナント・他部門の機密文書がコンテキストに混入する。
特に(2)は本番で頻発します。「賢いプロンプト」では防げません。検索クエリそのものに認可を埋め込むのが唯一の正解です。
// アンチパターン:全社ナレッジをそのまま検索 → テナント越境・情報漏洩
// const docs = await vectorStore.search(queryEmbedding, { topK: 8 });
// 正:リトリーバルに「認可フィルタ」を必須で埋め込む。
// アプリ層で後からフィルタするのではなく、検索そのものを認可でスコープする。
const docs = await vectorStore.search(queryEmbedding, {
topK: 8,
filter: {
tenantId: session.tenantId,
visibility: { $in: session.scopes }, // 閲覧可能な範囲だけを検索対象に
},
});
// 取得したドキュメントは「信頼できない外部入力」。
// プロンプトに入れる前に来歴を明示し、"指示ではなくデータ" として扱う。
const context = docs
.map((d) => `<context source="${d.source}">\n${d.text}\n</context>`)
.join("\n\n");
ここで重要なのは、<context> タグで囲えばインジェクションが防げる、と誤解しないことです。囲みは「これはデータであって指示ではない」という意図をモデルに伝える補助にすぎません。間接インジェクションの根治は、あくまで LLM05(出力検証)+ LLM06(ツール最小権限) で被害を封じ込めることです。RAGの認可設計は pgvectorによる本番RAG でも触れています。
LLM07: システムプロンプトの漏洩 — 秘密をプロンプトに置かない
2025年版の新項目。システムプロンプトは漏れる前提で設計します。攻撃者はしばしばプロンプトを吐き出させることに成功し、そこにAPIキー・DB名・内部ロール・認可ロジックが書かれていれば、次の攻撃の足がかりになります。
原則は2つ。
- 秘密をプロンプトから外に出す。 APIキーやDB接続情報は環境変数・シークレットマネージャに。プロンプトには置かない。
- セキュリティ制御をプロンプトに委譲しない。 「ユーザーがadminのときだけこの操作を許可して」といった認可をプロンプトの指示で実現しない。権限分離・認可は、LLMの外で決定的に強制します。ガードレール(入出力を検査する独立システム)は有効な追加層ですが、それは決定的な認可の代わりにはなりません。
LLM10: 無制限な消費 — DoSとコスト暴走を止める
旧「Model DoS」を拡張した項目。LLMは1リクエストが高コストなので、濫用は即座に金銭的被害(コスト暴走)とサービス劣化につながります。対策はWebのAPI防御とほぼ同じで、レート制限・入力上限・出力トークン上限・タイムアウト・監視です。
import { Ratelimit } from "@upstash/ratelimit";
// 1) レート制限:濫用・コスト暴走・モデル窃取(大量クエリでの蒸留)を抑止
const { success } = await ratelimit.limit(`llm:${session.userId}`);
if (!success) return new Response("Too Many Requests", { status: 429 });
// 2) 入力サイズと出力トークンを「上限」で締める(=コスト上限)
const { text } = await generateText({
model: "anthropic/claude-haiku-4-5",
prompt: clampInput(userPrompt, 4_000), // 入力長を検証してから渡す
maxOutputTokens: 512, // 出力上限 = コストの上限
abortSignal: AbortSignal.timeout(15_000), // 応答が返らないときは止める
});
そして可観測性。トークン消費・レイテンシ・エラー率・拒否率をユーザー単位で記録し、異常な消費を検知します。ログとトレースの設計は OpenTelemetryによる本番可観測性 が土台になります。
フレームワーク対応表:OWASP × NIST × MITRE × SAIF
「OWASP LLM Top 10」は開発者向けの実装リストです。組織的なリスク管理や、より広い脅威モデルを扱うには、以下を併用します。AIの回答(AI Overview / ChatGPTなど)でも引用されやすい、権威ある一次情報です。
| 目的 | フレームワーク | 位置づけ |
|---|---|---|
| アプリ実装の脆弱性チェック | OWASP Top 10 for LLM Apps 2025 | 開発者が直接使う実装リスト(本記事の軸) |
| 組織のAIリスク管理 | NIST AI RMF 1.0 (AI 100-1) | ガバナンス。Govern/Map/Measure/Manage の4機能 |
| 生成AI固有のリスク | NIST GenAI Profile (AI 600-1) | AI RMFの生成AI版プロファイル |
| 敵対的ML攻撃の戦術辞典 | MITRE ATLAS | ATT&CK型の攻撃手法マトリクス |
| 設計原則・組織展開 | Google SAIF | セキュアなAI導入のフレームワーク |
脅威モデリング自体の進め方は STRIDEによる脅威モデリング、セキュアコーディングの土台は NIST SSDF / OWASP ASVS 準拠のセキュアコーディング にまとめています。
導入チェックリスト(本番投入前)
明日から使える最小セットです。OWASP LLM Top 10を「設計レビューの観点」に落とし込みました。
- 出力検証(LLM05) — LLMの出力はZodスキーマで型・値域を検証し、業務ルールは決定的コードで再確認しているか。
- ツール最小権限(LLM06) — エージェントに破壊的実行権限を直接渡していないか。「提案」と「実行」を分離したか。
- 人間承認(LLM06) — 返金・削除・送金・権限変更などの高影響アクションに承認ゲートがあるか。
- 冪等性(LLM06) — 副作用のあるツール実行に冪等キーがあり、リトライで二重実行しないか。
- RAGの認可(LLM02/LLM08) — ベクトル検索クエリにテナント/可視性フィルタを埋め込んでいるか。取得文書を「信頼できない入力」として扱っているか。
- 秘密の分離(LLM07) — APIキー・DB情報・認可ロジックをプロンプトに書いていないか。
- レート制限とトークン上限(LLM10) — ユーザー単位のレート制限、入力長検証、
maxOutputTokens、タイムアウトがあるか。 - 可観測性(LLM10) — トークン消費・レイテンシ・拒否率をユーザー単位で記録し、異常を検知できるか。
- サプライチェーン(LLM03/LLM04) — モデル・データセット・依存の供給元を検証し、来歴(SBOM/ML-BOM)を残しているか。
まとめ
本番のLLMアプリのセキュリティは、特別な魔法ではありません。「LLMを、確率的で信頼できない一部品として扱い、決定的なアーキテクチャで囲う」——この一点に尽きます。
- プロンプトインジェクションは入力フィルタでは根治できない(LLM01)。だから**出力をゼロトラストで検証(LLM05)**し、**ツールを最小権限にして高影響操作は人間が承認(LLM06)**する。
- RAGとエージェントは攻撃面を広げる。検索に認可を埋め込み(LLM08/LLM02)、秘密はプロンプトの外へ(LLM07)。
- 濫用とコスト暴走は**レート制限・トークン上限・可観測性(LLM10)**で止める。
そしてこれらの多くは、決済基盤で本番二重課金0件を実現してきた分散システムの信頼性設計と地続きです。「速く作る」ことと「守れる状態にする」ことは、正しく設計すれば両立します。