2026年10月8日に、OWASP GenAI LLM Top 10の現行READMEと、canonicalなLLM01、LLM02、LLM03、LLM10を再読しました。2026版は2026年8月4日公開で、入力境界のPrompt Injectionは取得文書・ツール出力・画像・メモリにも及びます。開示は回答だけでなくツール引数・取得チャンク・ログ・テレメトリ・埋め込みに起こり、Excessive Agencyは機能・権限・自律性、Improper Output Handlingは下流へ渡す前の検証・サニタイズ・文脈別エンコードを扱います。これは脅威モデルの現行範囲であり、単一対策、製品診断、事故件数、法的適合を保証しません。
AIシステムのセキュリティベストプラクティス:安全なAI活用の実践ガイド
AIが社内資料を読めるようになったとき、先に決めたいのは「誰の資料を読めて、どこまで操作できるか」です。回答文を丁寧にする指示だけでは、別部署の文書の混入や、外部メールの誤送信を止められません。
この記事は、生成AIを使うアプリの担当者に向け、入力・検索・出力・実行の境界を整理します。公開情報の要約から始め、権限を増やす前に拒否されるべき操作を試す進め方です。モデル自体の学習防御や、企業ごとの法令適合を証明する資料ではありません。
30秒で要点
- 資料を渡す前に権限を検査する。 回答から消しても、すでにモデルへ渡した資料は取り戻せません。
- 生成と実行を分ける。 メール送信・削除・公開などの実行権限を、自由な文章やモデルの判断に委ねません。
- 暗号化・監視・回数制限はそれぞれ別の対策。 一つを導入しても、残りの境界が守られるとは限りません。
この画像はAIで生成した概念画です。実システムの構成図や、対策の安全性を認証した図ではありません。
守る対象と、使ってよい範囲を決める
「データを守る」には、本文だけでなく、検索された断片、添付、ログ、会話履歴、埋め込み、バックアップの扱いも含まれます。OWASPの2026年版は、最終回答以外も情報漏えいの経路になり得ると整理しています。機密情報の開示の正本
表が収まらない場合は、横にスクロールできます
| 対象 | 最初に確認すること | 小さく始める例 |
|---|---|---|
| 入力資料 | 公開範囲、利用目的、保存先、再利用条件 | 公開済みの商品説明だけを要約 |
| 検索用の文書 | 利用者ごとの閲覧権限、更新・失効の反映 | 部署別の許可文書だけを検索 |
| 出力とログ | 誰が読めるか、全文保存の必要性、保持・削除 | 操作IDと結果を残し、不要な本文を保存しない |
| 外部ツール | 読取・変更・送信の権限、宛先、上限 | 読取専用にし、送信機能を持たせない |
| 鍵とバックアップ | 鍵の保管、復旧担当、取り消し、復元方法 | 資料と鍵の管理責任を分け、復元を練習 |
公開情報で試せる工程に、顧客の実データを入れる必要はありません。必要な列だけにする、合成データに置き換えるなど、入力を減らす選択から考えます。外部サービスを使う場合は、契約・設定・実際のデータ経路で、保存や学習利用の条件を確認します。サービス名だけで一律に判断しません。
2026年版OWASPを、境界の点検に使う
公式リポジトリで2026年版の正本を確認しました。これはLLMアプリケーションのリスクを理解する資料で、会社ごとの危険度や対策効果のランキングではありません。別資料の「Top 10 for Agentic Applications」とも区別します。表が収まらない場合は、横にスクロールできます
| リスク | 起きることの例 | アプリ側の確認 |
|---|---|---|
| Prompt Injection(LLM01:2026) | 読んだページ内の指示が、要約以外の操作を促す | 外部資料を命令とせず、許可された操作を実行直前に検査 |
| Sensitive Information Disclosure(LLM02:2026) | 別利用者の検索断片やログが見える | 検索前の認可、必要最小の入力、ログとキャッシュの分離 |
| Excessive Agency(LLM03:2026) | 要約用のAIが削除や送信までできる | 機能・権限・自律性を減らし、必要な操作だけを許可 |
| Improper Output Handling(LLM10:2026) | 生成SQL・HTML・コマンドをそのまま実行する | 用途別の検証、出力のエンコード、限定された実行経路 |
プロンプト注入は、チャットの入力欄だけでは起きない
文書、メール、検索結果、画像、ツールの応答、保存された記憶も入口です。文字を数種類削除する処理や「指示に従わないで」というプロンプトだけで、防止できるとは考えません。OWASPは、モデルの指示境界が破られる可能性を前提に、周辺システムの権限で影響を制限する考え方を示しています。Prompt Injection
例えば、受信メールを要約するAIに送信ツールも渡すと、メール本文の不正な指示が外部送信につながるおそれがあります。要約だけなら読取専用にし、送信が必要な工程では宛先・本文・添付を人が確認した後に、アプリ側で実行権限を再検査します。Excessive Agency
3段階で導入する:把握、設計、継続確認
この図は編集部の点検手順です。上から順に確認しても、すべての攻撃を防げることを意味しません。
1.データと操作の経路を把握する
利用者、入力元、検索先、モデルへ渡る情報、保存先、外部へ出る先を一枚に書きます。「回答するだけ」と説明されていても、画像URLの自動取得、外部リンクのプレビュー、送信ツールなどで通信が発生する可能性があります。生成されたMarkdown画像などの自動取得も、出力処理の点検対象です。Improper Output Handling
守りたいものが閲覧権限なら、部署をまたぐ検索を拒否できるかを試します。費用なら、要求数だけでなく、入力・出力の量、再試行、並列数、ツール連鎖の上限を決めます。誤操作なら、実行できる種類・宛先・状態遷移を限定します。
2.モデルの外側に制御を置く
認証は「誰か」、認可は「この資料・操作を許可するか」です。利用者本人の権限を使い、代理処理や複数エージェントの連携でも範囲を引き継ぎます。出力がJSONの形を満たしていても、禁止された宛先や操作が含まれていれば拒否します。
重要な操作では、変更対象・差分・宛先・実行内容を具体的に示します。承認後に内容が変わったら、古い承認で実行しません。承認画面を設けるだけでなく、最終の実行経路が同じ条件を検査する必要があります。
3.拒否・停止・復旧を継続して確かめる
正常系に加え、失効した利用者、別部署の文書、誤った操作、過大入力、壊れた出力、同じ要求の再送を試します。担当者が気付けるログ、モデル呼出やツール実行の停止手段、以前の構成へ戻す方法を用意します。
監視では、権限拒否、連続失敗、呼出増加、費用上限への接近、想定外の宛先を見ます。ログへ資料全文や鍵を残してしまうと、監視自体が漏えい経路になります。記録の閲覧権限と保持期間も決めます。ツールの監視や回数制限は被害の制限に役立つ一方、認可の代わりにはなりません。
暗号化の例:FernetはAES-256ではない
暗号化は、鍵を持たない相手から保存内容を守るための一つの対策です。AIへ渡した後の誤開示、利用者の権限不足、誤送信を防ぐ仕組みとは別です。
Fernetの公式文書は、AES-128-CBCとHMAC-SHA256を使う構成を説明しています。鍵は32バイトをURL-safe Base64で表現しますが、それをAES-256と呼ぶのは誤りです。次は合成文字列だけを暗号化し、復号・誤鍵・破損・鍵更新を試す教材です。秘密鍵や暗号文を表示しません。必要なパッケージはcryptographyです。隔離したPython環境で用意し、次を一つのファイルとして実行できます。
from cryptography.fernet import Fernet, InvalidToken, MultiFernet
old_cipher = Fernet(Fernet.generate_key())
new_cipher = Fernet(Fernet.generate_key())
payload = "公開用の合成データ".encode("utf-8")
token = old_cipher.encrypt(payload)
assert old_cipher.decrypt(token) == payload
def must_reject(cipher, candidate):
try:
cipher.decrypt(candidate)
except InvalidToken:
return
raise AssertionError("invalid token was accepted")
must_reject(new_cipher, token)
must_reject(old_cipher, b"not-a-fernet-token")
# 最初の鍵で新しいトークンを暗号化し、旧鍵のトークンも読める構成。
rotation = MultiFernet([new_cipher, old_cipher])
rotated = rotation.rotate(token)
assert new_cipher.decrypt(rotated) == payload
must_reject(old_cipher, rotated)
print("roundtrip, wrong-key, malformed-token, rotation: passed")
ここでは毎回鍵を生成するため、終了後の復元はできません。本番でこの方法のまま鍵を失うと、保存した暗号文を読めなくなります。鍵の保管・アクセス・復旧・取り消しは別に設計し、旧鍵の廃止前に対象の更新とバックアップの復元を確かめます。Fernetはメッセージ全体をメモリに置くため、巨大ファイルの暗号化方式としてそのまま選びません。
APIの例:認証、認可、入力、回数を分ける
IPアドレスだけの回数制限は、利用者を識別する仕組みではありません。OWASP REST Security Cheat Sheetは、各エンドポイントのアクセス制御、入力形式・サイズの検証、要求過多への429応答などを分けて説明しています。
次の例は、架空の認証情報を固定したテスト専用アプリです。実際の認証方式を実装したものではありません。Flaskのtest_clientで、サーバを起動せずに拒否と受付を確かめます。モデルや外部サービスも呼びません。Flaskのテスト文書
from collections import defaultdict
from threading import Lock
from flask import Flask, request, jsonify
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
# 秘密ではない教材用の識別値。本番では検証済みIdP/セッションへ置き換える。
identities = {"demo-alice": "alice", "demo-bob": "bob"}
readers = {"public-demo": {"alice", "bob"}, "team-a-demo": {"alice"}}
now = [0.0] # テスト時計。運用では単調時計など適切な時計を使う。
windows = defaultdict(lambda: [0.0, 0])
lock = Lock()
def allowed(principal):
with lock:
start, count = windows[principal]
if now[0] - start >= 60:
start, count = now[0], 0
if count >= 2:
return False
windows[principal] = [start, count + 1]
return True
@app.post("/demo/prepare")
def prepare():
principal = identities.get(request.headers.get("X-Demo-Identity", ""))
if principal is None:
return jsonify(error="unauthenticated"), 401
if not request.is_json:
return jsonify(error="json required"), 415
data = request.get_json(silent=True)
if not isinstance(data, dict) or set(data) != {"document", "question"}:
return jsonify(error="invalid fields"), 400
doc, question = data["document"], data["question"]
if not isinstance(doc, str) or not isinstance(question, str):
return jsonify(error="invalid types"), 400
if not question.strip() or len(question) > 200:
return jsonify(error="invalid question length"), 400
if principal not in readers.get(doc, set()):
return jsonify(error="forbidden"), 403
if not allowed(principal):
return jsonify(error="rate limited"), 429
# 検査済み要求の受付だけ。資料検索・AI実行・送信は行わない。
return jsonify(status="accepted for demo"), 200
with app.test_client() as client:
body = {"document": "public-demo", "question": "合成資料の確認"}
alice = {"X-Demo-Identity": "demo-alice"}
bob = {"X-Demo-Identity": "demo-bob"}
assert client.post("/demo/prepare", json=body).status_code == 401
assert client.post("/demo/prepare", headers=alice, data="text").status_code == 415
assert client.post("/demo/prepare", headers=alice, json={}).status_code == 400
assert client.post("/demo/prepare", headers=bob,
json={**body, "document": "team-a-demo"}).status_code == 403
assert client.post("/demo/prepare", headers=alice, json=body).status_code == 200
assert client.post("/demo/prepare", headers=alice, json=body).status_code == 200
assert client.post("/demo/prepare", headers=alice, json=body).status_code == 429
assert client.post("/demo/prepare", headers=bob, json=body).status_code == 200
now[0] = 61.0
assert client.post("/demo/prepare", headers=alice, json=body).status_code == 200
print("identity, input, authorization, limits, reset: passed")
2回/60秒、200文字、4,096バイトは、拒否を見やすくする教材の条件です。推奨上限ではありません。メモリ上の状態は再起動で消え、複数プロセス間で共有されません。公開APIには、実認証、通信保護、共有された制限器、期限・並列数・量・費用の管理、監査、再送や状態遷移の制御が別途必要です。Flaskの開発サーバを本番用に使う説明でもありません。配備の公式文書
業務別の試験例と、使いやすさの両立
次の3例は編集部が作成した架空の試験計画です。First byteの導入実績や事故削減率ではありません。
表が収まらない場合は、横にスクロールできます
| 業務 | まず許可する範囲 | 拒否・回復で確かめること |
|---|---|---|
| 顧客対応の下書き | 公開FAQを読む、回答案を作る | 実顧客情報を混ぜない、別顧客の履歴を読めない、送信は確認後 |
| 社内資料の検索 | 利用者が閲覧できる現行文書のみ | 異動・失効後に検索もキャッシュも使えない、旧版を回答根拠にしない |
| 広報コンテンツ | 公開資料から案を作る | 未確認数値を保留、宛先・添付・公開内容の変更で再確認、公開停止ができる |
使いやすさのためには、禁止事項だけでなく「公開資料ならここで試せる」「下書きまでは作れる」「この操作はこの担当者が確認する」と許可範囲を示します。確認を増やすほどよいとは限りません。影響の小さい可逆な処理と、外部に出る重要操作を分け、実際の失敗・手戻り・確認工数を記録して調整します。
次の一手:一つの経路で、拒否を確かめる
最初は、公開資料一つ、利用者二人、読取だけという条件で始めます。正常な要求が通ること、許可外の資料がモデルへ渡る前に止まること、過大な要求が拒否されること、停止後に担当者が復旧できることを確認します。合格条件と残るリスクを記録してから、対象や権限を増やします。
役割・承認・監査の決め方は生成AI導入の責任分界、業務側のつまずきはAI導入の失敗を防ぐポイント、進め方はFirst byte流AI活用術へ。コードの成功を、組織全体の安全や法令適合の証明に置き換えず、対象業務の経路で確認を続けます。
