メインコンテンツへスキップ
ブログ一覧に戻る
AI活用・LLM

AIシステムのセキュリティベストプラクティス:安全なAI活用の実践ガイド

2025年12月3日
最終更新: 2026年10月8日
11分で読めます
AIシステムのセキュリティベストプラクティス:安全なAI活用の実践ガイド

この記事でわかること

生成AIアプリの安全対策を、入力資料・検索権限・出力・実行の境界から解説。OWASPの2026年版を参照し、Fernetの暗号化とFlaskのAPI検査を、モデルや外部サービスにつながないコードで確認します。監視・停止・復旧、担当者の承認、ログや鍵の管理も分け、公開資料で小さく試す手順と対策の限界を整理します。

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への入力、操作の実行を別々の境界として確認する概念画
AIで生成した概念画。実システムの構成図や安全性の認証ではありません。

この画像は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段階で導入する:把握、設計、継続確認

入力資料の利用許可、検索前の権限確認、出力の検証、重要操作の承認、ログと停止の5段階を示す図
編集部の点検手順。全攻撃の防止を保証する図ではありません。

この図は編集部の点検手順です。上から順に確認しても、すべての攻撃を防げることを意味しません。

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活用術へ。コードの成功を、組織全体の安全や法令適合の証明に置き換えず、対象業務の経路で確認を続けます。

よくある質問(FAQ)