はじめに
LLMを活用したシステム、特にRAG (Retrieval-Augmented Generation) では、外部知識の検索に「ベクトル検索」が使われます。しかし、この検索プロセスには、従来のSQLインジェクションとは異なる、セマンティック(意味論的)な脆弱性が潜んでいます。
本記事では、攻撃者が「正しい文書を書き換えることなく、検索結果を操作する」手法であるセマンティック・ポイズニングの仕組みを、数式とコードで解説します。
1. ベクトル検索の「盲点」
多くのRAGシステムは、ユーザーのクエリをベクトル化し、コサイン類似度などで上位 $k$ 件(Top-k)のドキュメントを抽出します。
$$\text{similarity}(\mathbf{q}, \mathbf{d}) = \frac{\mathbf{q} \cdot \mathbf{d}}{|\mathbf{q}| |\mathbf{d}|}$$
ここで重要なのは、ベクトルデータベースは「情報の正確性」や「権威性」を一切見ていないという点です。単に「数学的に近いか」だけを判断基準にしています。
2. 攻撃のシナリオ:ランキングの乗っ取り
攻撃者は、正当な文書を削除・改ざんする必要はありません。ただ、「よりクエリに近いベクトルを持つ汚染ドキュメント」をデータベースに注入するだけで十分です。
システムが上位3件を参照する場合、汚染ドキュメントが1位〜3位を独占すれば、LLMには汚染された情報のみが渡され、正当な情報は「存在しないもの」として扱われます。
3. 実装による概念実証 (PoC)
PythonとNumPyを使用して、この「ランキングの逆転」をシミュレーションしてみます。
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# クエリ:ユーザーが「最新のセキュリティ対策」について質問
query = np.array([0.20, 0.80])
# 正当な文書:正しいセキュリティ手順
doc_legit = np.array([0.10, 0.70])
# 汚染文書:攻撃者が用意した、わずかにクエリに「寄寄せた」文書
# 意味的には近く見えるが、内容は悪意があるもの
doc_poisoned = np.array([0.21, 0.79])
# スコア計算
score_legit = cosine_similarity(query, doc_legit)
score_poisoned = cosine_similarity(query, doc_poisoned)
print(f"Legitimate Doc Score: {score_legit:.6f}")
print(f"Poisoned Doc Score: {score_poisoned:.6f}")
if score_poisoned > score_legit:
print("\n[ALERT] 汚染文書がランキング1位を獲得しました。")
print("LLMには攻撃者の用意したコンテキストが優先的に渡されます。")
実行結果
Legitimate Doc Score: 0.997054
Poisoned Doc Score: 0.999992
[ALERT] 汚染文書がランキング1位を獲得しました。
数百・数千次元の空間では、特定のキーワードを微妙に調整(アドバーサリアル・パッチ)するだけで、特定のクエリに対して驚異的な類似度を叩き出すことが可能になります。
4. 対策の方向性
この問題は「検索アルゴリズムの仕様」に起因するため、完全な防御は困難ですが、以下の多層防御が推奨されます。
- 信頼スコアの導入: ベクトル距離だけでなく、ドキュメントの最終更新日や信頼できるソース(メタデータ)に基づく重み付けを行う。
- 多様性のある検索 (Maximal Marginal Relevance): 似たような内容ばかりを抽出せず、あえてベクトルの方向性が異なるドキュメントを混ぜる。
- 入力バリデーション: 埋め込みモデルに渡す前に、プロンプトインジェクションと同様の検閲レイヤーを設ける。
おわりに
RAGの普及により、ベクトルDBはシステムの「脳」の一部となりました。しかし、そのランキングプロセスを制御されることは、AIの判断を完全にコントロールされることに等しいのです。開発者は「検索結果が常に正しい」という前提を疑う必要があります。