Webページの本文だけでなく、画像や音声もAIに渡して要約している人へ。出力に含まれるURLは、誰がリンクに変えていますか。
2026年9月21日、NVIDIA AI Red Teamは、テキスト・画像・音声の3種類がそろったときだけ狙った出力を引き出すプロンプト注入の実験を公開しました。対象はGemma4 E4Bです。ページ要約アプリの実験では、要約に当選を装う文言とURLが現れ、画面上でクリック可能になりました。
結論から言うと
要約AIに画像や音声を渡すなら、入力の検査に加えて、モデルの出力がアプリの機能になる場所を見直す必要があります。
この記事で選ぶのは、本文を文字列として表示し、リンク先はアプリ側の承認済み一覧からだけ作る設計です。リンク先のURLはモデルから受け取らず、承認済み資料のIDだけを選ばせます。
これで制御するのは、生成された任意のURLをクリック可能にする経路です。文章による誘導や要約の誤りは、別に扱う必要があります。

3種類の入力の重なりから別の指示が現れることを表したAI生成イラスト。実験に使われた画像ではありません。
画像から文字を抜き出しても、見えないものがある
NVIDIAの実験手順では、モデルを固定し、画像の画素と音声の波形を最適化しています。3種類を合わせた条件では目標文へ近づけ、それ以外の組み合わせでは目標文から遠ざけるようにします。
保存したPNGとWAVを読み直しても結果を確認し、単独入力と2種類の組み合わせでは目標文が出ませんでした。OCRや音声認識でも目標文は現れず、実験用の個別入力チェックは通過しました。一方、生成後の検査は、報告された2例の出力違反を検出しています。
これは、特定のモデル・入力順・素材・プロンプトでの報告です。ほかのモデルへの転用や、各種ガードレール製品全体の突破を示すものではありません。この記事では攻撃自体を追試していません。
ここで、自分のアプリに置き換えてみてください。
画像から取り出した文字を検査する処理と、画像そのものを受け取るモデルは、同じ入力を見ていません。文字起こしだけを検査する場合も同じです。検査用に情報を落とした表現で問題が見つからなくても、モデルへ渡す情報全体の判定にはなりません。
入力を分類する仕組みは残す。そのうえで、別の場所にも制御を置く。今回は、その場所を要約の表示処理に絞ります。
URLを書いたのはモデル。リンクにしたのはアプリ
要約にURLが混じることと、そのURLへ移動できることの間には、アプリの処理が1つあります。
たとえば、生成結果をMarkdownレンダラーへ渡す実装です。リンク記法を解釈する設定なら、モデルが作った文字列がリンクになります。裸のURLを自動リンク化する処理でも、同じ境界を越えます。
これは、出力の内容だけを分類していては見落としやすい箇所です。文章が安全かどうかに加えて、その文章を受け取ったコンポーネントが何をするかを調べる必要があります。
OWASPのImproper Output Handlingも、モデルの出力を後段に渡す際の検証と、利用先に応じたエンコードを求めています。HTMLを作るならHTMLとして、データベースに渡すならデータベースの操作として、境界ごとの扱いが必要です。
要約画面について、次の3つは別々に決められます。
| モデルに任せるもの | アプリで固定できるもの | 失う自由度 |
|---|---|---|
| 要約の文章 | 文字列としてだけ表示する | 本文内の自由な装飾や埋め込み |
| 参照する資料のID | IDに対応するURLと表示名 | モデルが新しいリンク先を提案する機能 |
| 回答の生成 | 検証完了後に表示を開始する | 未検証の文章を即座に流す体験 |
参照元を示すことが目的なら、自由なURL生成までモデルに渡す理由は薄くなります。資料の候補を増やす処理と、要約を作る処理を分ければよいからです。
一方、リンク先の発見そのものが商品の中心なら、この制約は機能を削ります。その場合は候補URLをいったん未承認として受け取り、表示・アクセスの審査を別工程にする設計が必要です。
本文とリンク先を、別のデータ経路にする
ここからはNVIDIAの実装ではなく、報告を受けた設計例です。対象は、あらかじめ承認した資料の範囲で要約を返すWebアプリとします。
モデルから受け取るデータは、次の2項目だけにします。
{
"summary": "資料の要点をここに書く",
"source_ids": ["s1"]
}
summary は表示する文章、source_ids は今回のリクエストで使える資料IDです。URL、HTML、リンクの表示名を受け取る専用フィールドは作りません。
資料一覧は、ページから見つかったURLを無条件に登録する場所ではありません。利用者と処理目的に応じて、アプリが許可したものを渡します。モデルが一覧を書き換えられたら、この分離は成立しません。
以下は標準ライブラリだけで動くPythonの最小例です。example.com のURLは説明用の固定値で、アクセスは行いません。
from html import escape
def render_summary(result, approved_sources):
# JSONのデコード後に呼ぶ。余分なフィールドも受け入れない。
if type(result) is not dict:
raise ValueError("object required")
if set(result) != {"summary", "source_ids"}:
raise ValueError("unexpected fields")
summary = result["summary"]
ids = result["source_ids"]
if type(summary) is not str or len(summary) > 4000:
raise ValueError("invalid summary")
if type(ids) is not list or len(ids) > 5:
raise ValueError("invalid source_ids")
if any(type(sid) is not str or sid not in approved_sources
for sid in ids):
raise ValueError("unapproved source")
# 本文はエスケープする。Markdownや自動リンク化には渡さない。
paragraph = f"<p>{escape(summary)}</p>"
links = []
for sid in dict.fromkeys(ids):
label, url = approved_sources[sid]
links.append(
f'<li><a href="{escape(url, quote=True)}">'
f'{escape(label)}</a></li>'
)
return paragraph + "<ul>" + "".join(links) + "</ul>"
# サーバーが今回の利用者・リクエスト用に決めた一覧。
# 外部ページやモデルの応答から、そのまま作ってはいけない。
approved = {
"s1": ("承認済み資料 — example.com", "https://example.com/manual"),
}
result = {"summary": "資料の要点です。", "source_ids": ["s1"]}
print(render_summary(result, approved))
決定的なのは、href に入る値を approved_sources[sid] からしか取っていないことです。モデルの生成文をURLのパスやクエリに継ぎ足す処理もありません。
本文に [こちら](https://unapproved.invalid) と書かれても、Markdownとして解釈する工程がないので文字列のままです。<a> が書かれた場合も、Pythonのhtml.escapeでHTMLの特殊文字をエスケープします。
この例の保証は、この関数が作るリンクのURLは、渡された承認済み一覧の値に限られるというものです。あとからフロントエンドが本文を再びMarkdown化したり、自動リンク化したりすれば、その保証は失われます。
一覧に登録済みのサイトが別の場所へリダイレクトする問題や、モデルが無関係な資料IDを選んでしまう問題も残ります。出力するURLの制限、移動先の信頼性、引用の正しさは、それぞれ確認してください。
正しいJSONでも、誘導文は通る
掲載コードはローカルで実行し、HTMLパーサーで生成されたリンク先を確認しました。確認したのは次の動作です。実ブラウザーやモデル推論を含む試験ではありません。
| 入力 | この関数の動作 |
|---|---|
承認済みの s1
|
一覧にあるURLだけを出力 |
| 本文中のHTMLリンク、Markdownリンク、裸のURL、画像記法 | 本文から追加のリンク・画像要素を作らない |
| 未登録ID、URLそのものをIDに指定 | 拒否 |
IDの型が違う、余分な url フィールドがある |
拒否 |
誘導的な文章と、承認済みの s1
|
受け入れる |
最後の行は、意図的に残した境界です。
たとえば「今すぐ別サイトを検索してください」という文章には、新しいクリック先がなくても誘導があります。この関数は文章の意味を判定しないため、それを検出できません。
用途に応じた内容の検査は、この表示処理の前に置きます。OWASPのプロンプト注入対策も、入力検査だけでなく出力監視や最小権限を組み合わせています。要約専用のアプリなら、そもそも送金やメール送信の権限を付けない、という制約も有効な設計判断です。
ストリーミングにも注意が必要です。完成したJSONを検査する前に、途中の文字列をリッチ表示してしまうと、後段の拒否より先にリンクが現れる実装になります。この設計では生成完了まで待ち、検査に失敗したら固定のエラー表示にします。生の応答を代わりに表示するフォールバックは作りません。
入力の組み合わせは試験で、出力の権限は実装で絞る
マルチモーダル入力の評価には、1種類ずつ外す試験も加えたいところです。ただし、毎回の本番リクエストで全組み合わせを推論させる設計とは分けて考えます。
入力の種類を $M$ とすると、空集合を除いた組み合わせは $2^M-1$ 通りです。3種類なら7通り、4種類なら15通りになります。これは組み合わせの個数であり、推論時間がそのまま7倍や15倍になるという計測値ではありません。
開発時は、同じ本文・素材・システム指示・生成設定を固定し、入力の有無だけを変えて、要約の正しさと出力契約の違反を記録します。目標文が出なかったケースでも、別の不適切な応答がないかは確認します。
本番では、業務に必要なメディアだけを渡し、生成後の検査と表示側の制約を毎回適用する。音声を外すなら、音声にしかない情報を捨てる影響も評価する。この組み合わせなら、機能・評価コスト・実行時の制御を個別に判断できます。
まず、自分の要約画面で次の3か所を追ってください。
- ページの画像や音声を、どこでモデル入力へ足しているか。
- 生成文を、どこでHTML・Markdown・クリック可能なURLへ変えているか。
- その直前に、アプリが許可した操作やリンク先を強制する処理があるか。
モデルの入力が豊かになるほど、出力の使い道をアプリ側で明確にする価値が増します。
要約を書く能力を渡しても、任意の移動先を作る権限まで渡す必要はありません。
参考資料
NVIDIA AI Red Team — When Modalities Combine: The Combinatorial Blind Spot in AI Security(2026年9月21日)
https://research.nvidia.com/ai-security/when-modalities-combine-combinatorial-blind-spot-ai-security
OWASP Gen AI Security Project — LLM05:2025 Improper Output Handling
https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/
OWASP Cheat Sheet Series — LLM Prompt Injection Prevention Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html
Python Documentation — html: HyperText Markup Language support
https://docs.python.org/3/library/html.html