评估向量和嵌入弱点
仅限授权使用: 这些测试与您拥有或获授权评估的RAG系统中的向量存储和嵌入模型进行交互。对您无法控制的系统进行嵌入反转和跨租户探测可能会暴露第三方数据,未经授权禁止此类行为。
概述
检索增强生成(RAG)系统将文档转换为嵌入向量,存储在向量数据库(Pinecone、Qdrant、Weaviate、Chroma、pgvector、FAISS)中,并检索最近的向量以支撑LLM响应。OWASP LLM08:2025 向量和嵌入弱点 涵盖了该层独有的安全风险:
- 嵌入反转 — 嵌入并非单向的。训练好的反转模型(或黑盒重建攻击)可以从其向量中恢复原始文本的大部分内容,从而泄露源文档(映射到MITRE ATLAS AML.T0024.001 反转ML模型)。
- 成员推断 — 查询特定记录是否存在于语料库中(AML.T0024.000)。
- 跨租户/多租户泄露 — 当一个命名空间/集合共享或过滤隔离缺失时,一个租户可能检索到另一个租户的数据块。
- 知识库投毒 — 能够写入语料库的攻击者插入精心构造的数据块,这些数据块在检索中占据主导地位(与预期查询具有高余弦相似度),并携带间接提示注入负载。
- 检索操纵 — 旨在针对许多不相关查询被检索到的对抗性文档(“检索劫持”)。
父技术是 AML.T0024 — 通过ML推理API进行数据窃取:攻击者利用合法的推理/查询访问权限窃取数据(通过反转、成员推断或模型提取获取源文本)。此技能提供了对所有五类弱点可重复的评估方法。
何时使用
- 在对任何RAG/向量搜索应用进行安全评估时(覆盖OWASP LLM08)。
- 向量存储是多租户的,并且您需要证明命名空间/元数据隔离时。
- 语料库接受用户提交或第三方文档时(投毒面)。
- 嵌入端点可从外部访问时(反转/成员推断面)。
- 在上线前验证检索过滤控制时。
前提条件
- 涵盖目标嵌入端点和向量存储的授权和范围。
- Python 3.10+。
- 对测试集合的读取(以及投毒测试的写入)权限——切勿在生产语料库上执行。
# 向量数据库客户端 + 嵌入 + 相似度工具
python -m pip install numpy scikit-learn sentence-transformers
python -m pip install qdrant-client chromadb pinecone-client weaviate-client
# (可选)文本嵌入反转研究基线
python -m pip install vec2text
目标
- 测量目标嵌入模型上的嵌入反转暴露程度。
- 针对语料库运行成员推断探测。
- 测试多租户隔离(命名空间、元数据过滤器、RBAC)是否存在跨租户泄露。
- 向测试集合注入良性的投毒数据块并测量检索主导性。
- 检测检索到的数据块中残留的间接提示注入内容。
- 推荐控制措施:租户范围过滤器、内容验证、嵌入访问限制。
MITRE ATT&CK 映射
| ID | 战术 | 官方技术名称 | 在本技能中的作用 |
|---|---|---|---|
| AML.T0024 | ATLAS:数据窃取 | 通过ML推理API进行数据窃取 | 利用查询/嵌入访问窃取源数据 |
| AML.T0024.000 | ATLAS:数据窃取 | 推断训练数据成员身份 | 针对语料库的成员推断探测 |
| AML.T0024.001 | ATLAS:数据窃取 | 反转ML模型 | 源文本的嵌入反转重建 |
| AML.T0020 | ATLAS:资源开发 | 投毒训练数据 | 语料库的知识库投毒 |
| AML.T0051.001 | ATLAS:初始访问 | LLM提示注入:间接 | 嵌入在检索到的数据块中的注入负载 |
工作流程
步骤1:盘点RAG流水线
记录嵌入模型+维度、向量存储及其租户模型、分块策略、检索top_k和相似度度量(余弦/点积/L2),以及查询时应用的任何元数据过滤器。
# 示例:检查Qdrant集合
from qdrant_client import QdrantClient
client = QdrantClient(url="http://localhost:6333")
info = client.get_collection("docs")
print(info.config.params.vectors) # 大小 + 距离度量
print(client.count("docs")) # 语料库大小
步骤2:测试嵌入反转暴露程度
相似文本的嵌入相近;拥有嵌入端点的攻击者可以迭代地重建文本,使其嵌入与目标向量匹配。通过候选重建与目标之间的余弦相似度,测量嵌入空间中最近邻恢复的程度。
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
model = SentenceTransformer("all-MiniLM-L6-v2")
secret = "Patient John Doe, MRN 553120, diagnosed with hypertension."
target_vec = model.encode([secret])
# 攻击者只有target_vec和嵌入端点。爬山法优化候选文本。
candidates = [
"Patient name and medical record number with a diagnosis.",
"John Doe medical record hypertension diagnosis",
"Patient John Doe MRN diagnosed hypertension",
]
cand_vecs = model.encode(candidates)
sims = cosine_similarity(target_vec, cand_vecs)[0]
for c, s in sorted(zip(candidates, sims), key=lambda x: -x[1]):
print(f"{s:.3f} {c}")
# 对于近乎逐字猜中的内容,相似度很高 => 该模型的嵌入反转风险真实存在。
对于研究级重建基线,可以针对兼容的嵌入模型使用vec2text来演示全文恢复。
步骤3:成员推断
通过测量精确引用查询的top-1检索相似度,判断特定文档是否在语料库中:语料库内项目的最大相似度显著高于语料库外对照项。
def membership_score(client, collection, embed, text):
vec = embed([text])[0].tolist()
hits = client.search(collection_name=collection, query_vector=vec, limit=1)
return hits[0].score if hits else 0.0
in_corpus = membership_score(client, "docs", model.encode, "<来自已知块的精确引用>")
control = membership_score(client, "docs", model.encode, "一条无关的随机句子。")
print(f"语料库内={in_corpus:.3f} 对照={control:.3f} 差值={in_corpus-control:.3f}")
# 较大的正差值表明该项在语料库中(成员身份泄露)。
步骤4:测试多租户隔离
确认租户B无法检索租户A的数据块。发出租户B认证的查询(应被过滤),并确保结果中未出现租户A的tenant_id。
# 以租户B身份查询;期望仅返回tenant_id == "B"的结果。
from qdrant_client.models import Filter, FieldCondition, MatchValue
vec = model.encode(["confidential salary information"])[0].tolist()
hits = client.search(
collection_name="docs",
query_vector=vec,
limit=10,
query_filter=Filter(must=[FieldCondition(key="tenant_id", match=MatchValue(value="B"))]),
)
leaked = [h for h in hits if h.payload.get("tenant_id") != "B"]
print("跨租户泄露" if leaked else "隔离正常", "->", len(leaked), "个外部行")
# 关键测试:不加过滤器重复上述操作,以确认隔离由服务器而非客户端强制执行。
# 如果无过滤查询返回租户A的数据,则隔离仅为客户端侧实现。
hits_nofilter = client.search(collection_name="docs", query_vector=vec, limit=10)
print("服务器端隔离失败" if any(h.payload.get("tenant_id") != "B" for h in hits_nofilter) else "正常")
步骤5:知识库投毒(仅限测试集合)
插入一个良性的投毒数据块,该数据块被设计为能针对许多不相关查询被检索到,然后测量其在top_k中出现的频率。
from qdrant_client.models import PointStruct
# 良性标记负载(无实际注入),用于测量检索主导性。
poison = "中毒-金丝雀. " + " ".join(
["password reset billing refund account login support error help"] * 8
)
client.upsert("docs_test", points=[
PointStruct(id=999999, vector=model.encode([poison])[0].tolist(),
payload={"tenant_id": "B", "source": "中毒测试"})
])
queries = ["how do I get a refund", "reset my password", "what is the weather"]
for q in queries:
hits = client.search("docs_test", model.encode([q])[0].tolist(), limit=5)
dominated = any(h.payload.get("source") == "中毒测试" for h in hits)
print(f"{'中毒数据在top5中' if dominated else '正常'}: {q}")
步骤6:检测检索到的数据块中的间接提示注入
在检索到的数据块文本拼接到提示之前,扫描其中的注入标记。
import re
注入模式 = [
r"ignore (all|previous|the above) instructions",
r"system prompt", r"you are now", r"disregard", r"</?(system|instructions)>",
]
def chunk_is_injection(text):
low = text.lower()
return [p for p in 注入模式 if re.search(p, low)]
for hit in client.search("docs", model.encode(["help"])[0].tolist(), limit=10):
flags = chunk_is_injection(hit.payload.get("text", ""))
if flags:
print("数据块中存在间接注入", hit.id, flags)
步骤7:报告和修复
- 反转/成员推断: 对嵌入端点进行速率限制和身份验证;避免返回原始相似度分数;限制谁可以查询嵌入。
- 跨租户: 在服务器端强制执行租户过滤器(如可行,为每个租户使用单独的集合/命名空间);永不依赖客户端提供的过滤器。
- 投毒: 验证并标记每个摄入的数据块的来源;扫描输入以防止注入;限制任何单一来源在检索中的份额。
- 间接注入: 净化检索到的数据块并应用输出护栏(参见
defending-llms-with-guardrails)。
工具和资源
| 工具 | 用途 | 主要来源 |
|---|---|---|
| OWASP LLM08 | 向量和嵌入弱点指南 | https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/ |
| sentence-transformers | 用于测试的嵌入生成 | https://www.sbert.net/ |
| Qdrant 客户端 | 向量存储 + 过滤搜索 | https://qdrant.tech/documentation/ |
| Chroma / Weaviate / Pinecone | 替代向量存储 | https://docs.trychroma.com/ |
| vec2text | 嵌入反转研究基线 | https://github.com/jxmorris12/vec2text |
| MITRE ATLAS | AML.T0024 通过ML推理API进行数据窃取 | https://atlas.mitre.org/ |
验证标准
- RAG流水线已盘点(嵌入模型、存储、租户模型、度量、top_k、过滤器)。
- 嵌入反转暴露程度已测量并评级。
- 语料库内与对照项的成员推断差值已计算。
- 多租户隔离已测试(带和不带客户端过滤器,确认服务器端执行)。
- 投毒主导性已仅在测试集合中测量。
- 检索到的数据块已扫描间接注入内容。
- 针对每个弱点类别的发现已报告并附带修复建议。
- 评估期间未修改生产语料库。