标签 缓存攻击 下的文章

0x01 研究背景

在自回归生成模型(Autoregressive Model)中,LLM每生成一个新token,都会将此前生成的序列作为输入。若每一步都重新计算全部注意力(Q、K、V 矩阵),计算量将随序列长度平方级增长。在长上下文和高并发场景下,这一开销会迅速成为系统瓶颈。为此,主流推理框架普遍引入KV-Cache技术。 KV-Cache通过缓存此前token的Key(K)和Value(V)向量,在下一步生成时只需计算新的Query(Q),即可直接复用前面的K/V,从而显著降低重复计算量。实践中,KV-Cache 通常能在保持模型精度不变的前提下,带来约5-8倍的推理加速。这一机制已经成为vLLM、SGLang、DeepSpeed-Inference等高性能推理引擎,以及Hugging Face generate(use_cache=True)接口的默认能力。

随着2024–2025年多租户推理服务(如vLLM、SGLang、TensorRT-LLM)的大规模部署,系统在单模型、多租户共享的前提下,又进一步引入跨请求的前缀缓存共享(prefix caching)。当不同请求的prompt存在相同前缀时,系统可以直接复用已有KV-Cache,大幅摊薄Prefill成本并提升吞吐。然而,当这种共享与复用机制扩展到多租户并发环境时,KV-Cache不再只是一个“性能优化组件”,而是演变成新的攻击面:攻击者可以通过观测Prefill 时间、TTFT等性能差异发起时序侧信道攻击,通过篡改缓存内容实施History Swapping(生成轨迹劫持),或者通过对Key向量注入扰动发动Cache Corruption(缓存腐败),从而导致跨租户信息泄露、话题漂移甚至下游任务性能显著下降。

0x02 KV缓存工作机制与共享复用原理

下面是KV-Cache工作原理的示意图。

KV-Cache工作原理

KV-Cache工作原理图

接下来我们用文字详细拆解,更深入了解KV缓存工作机制。

2.1 两阶段推理:Prefill与Decode

KV-Cache的核心做法分为两阶段。

(1)Prefill阶段(Prompt阶段)一次性计算输入序列的K/V并写入缓存 模型读取完整输入的prompt,计算出所有token的Key/Value向量并写入缓存。 公式表示为:

image-20251229205745845

此时缓存中的K/V向量构成了后续生成阶段的基础。

(2)Decode阶段(生成阶段)仅对新token计算Q/K/V,并复用历史K/V完成注意力计算 当模型生成新token时,仅需计算该token对应的Q、K、V向量。

image-20251229205757073

然后与缓存中已有的K/V拼接,直接完成注意力计算。这样便避免了重复计算前面N−1个token的注意力结果。

2.2 past_key_values

在Hugging Face Transformers框架中,KV-Cache在接口层面通过 past_key_values 对象实现。该对象并非一个抽象的控制开关,而是模型前向推理过程中实际生成、并可跨生成步骤复用的中间状态。它以分层的结构保存已处理历史Token的Key和Value张量,从而支撑自回归生成的增量计算。

从结构上看,past_key_values通常是一个长度为模型层数的列表或元组,其中每一层对应一对 (K, V)张量。不同模型的具体维度布局可能存在差异,但其核心语义一致:存储历史序列的注意力键值表示,以便后续生成时直接复用。

在推理流程中,Prefill 阶段会对完整的提示词进行计算,并首次生成past_key_values。进入Decode阶段后,若将此缓存作为输入传递给模型,模型通常只需为新输入的Token计算其对应的Key和Value,并将其追加至现有缓存末尾,从而避免了历史部分的重复计算。这种基于past_key_values的复用是框架的原生机制,其带来的加速直接源于注意力计算的真实削减,因此更适合作为评估系统性能及分析相关安全影响的工程基准。相比之下,通过sleep()或人为插桩制造“快慢差异”的方法仅能模拟现象,难以反映实际推理系统的缓存行为。此外,Transformers框架的generate()接口通常通过参数use_cache=True来启用此缓存机制。vLLM、SGLang、DeepSpeed-Inference在系统层面也普遍实现了类似机制,以降低生成延迟并提升吞吐量。

2.3 多租户场景下的前缀缓存与最长前缀匹配

在多请求并发且显存资源受限的推理服务中,为提升吞吐并降低重复的Prefill开销,系统常采用前缀缓存策略。其核心思想是当新请求的提示词(更准确地说是其Token序列)与某条已缓存的序列存在前缀重合时,系统可直接复用该前缀部分对应的KV-Cache,仅需对未命中的后续Token执行增量计算。

当缓存池中存在多个可能的候选前缀时,命中判定通常遵循最长前缀匹配(LPM)原则:在所有缓存条目中,系统会选择与新请求Token序列匹配长度最长的那一条作为复用对象,以最大化缓存利用率,减少重复计算。在工程实现上,这依赖于能够高效进行Token序列前缀匹配的数据结构或索引机制,例如前缀树(Trie)、基于前N个Token的分层哈希,或基于序列哈希值的多级索引。

根据匹配程度,命中效果可分为两类:一是完全命中,即请求的绝大部分或全部前缀已在缓存中,Prefill阶段的计算量显著下降;二是部分命中,即仅能复用较短的前缀,系统仍需对剩余后缀执行完整的Prefill计算。无论是“是否命中”还是“命中长度”,都会直接反映在可观测的系统性能指标上,例如Prefill时间、首Token延迟的分布等。

当前主流引擎(如vLLM的PagedAttention、LMCache)进一步通过分页管理和压缩技术缓解显存碎片,但前缀共享引入的侧信道与内存安全风险依然突出,这也是后续攻击面的根源。

0x03 KV-Cache的主要攻击面原理介绍

在理解KV-Cache的核心优化机制与共享原理后,我们可以看到其高效性背后隐藏的脆弱性。下面详解三大主要攻击面:时序侧信道攻击、操纵攻击与腐败攻击。

3.1 KV-Cache时序侧信道攻击

在共享KV-Cache的系统中,攻击者通过测量响应时间或请求处理顺序,推断缓存是否命中(hit),从而还原其他用户的Prompt(提示词)。

image-20251028173225854

时序侧信道攻击完流程图

设定还原的语句是"Imagine you are an IT expert",攻击者已经成功还原出"Imagine you are",并尝试还原下一个token "an"。下面我们根据上图分步骤拆解一下攻击过程。

步骤1:Generate candidates

攻击者在本地用小模型、模板或启发式方法生成可能的下一个token候选集合,例如:

  • Imagine you are an
  • Imagine you are a
  • Imagine you are the

把未知的victim prompt逐步转化为一系列候选前缀/后缀,便于后续probe。优点是减少搜索空间。


步骤2:Generate dummy

  • Candidate请求:每个请求包含一个候选后缀(比如Imagine you are an)。目标是看哪一个candidate与victim的缓存前缀最长匹配而“命中”缓存。
  • Dummy请求:随机或不相关的prompt(用来制造队列/填充调度槽位),以便控制调度顺序或避免直接暴露自己的probe请求导致缓存污染判断混淆。

步骤3:Send three request batches in turn

攻击者按这个顺序把三组请求发到服务器(可能是同一API key,也可能跨多个短时间窗口发出)。核心就是在调度队列里把candidate放在中间,观察它是否因为缓存命中而更快返回。

步骤4:Observe the returning order

攻击者记录三批请求的返回顺序和时间(TTFT/latency)。若candidate的响应比其前后的dummy显著更快或优先到达,就可推断该candidate是命中了缓存(即victim的prompt与该candidate共享较长前缀)。

3.2 History Swapping 攻击

image-20251230101845847

History Swapping操纵攻击原理图

攻击者通过结构化替换或注入KV-Cache内容,来“劫持”模型的生成轨迹,强制引导输出转向攻击者指定的主题或行为。这种攻击利用KV-Cache编码了不仅仅是上下文,还包括话题规划(topic trajectory)和结构化推理(structural planning)的特性。

设定攻击场景:受害者Prompt为“Give a precise technical explanation of espresso extraction variables”(讨论咖啡萃取),攻击者希望劫持输出到恒星生命周期主题。用户可见Prompt不变。

步骤1: 预生成目标主题KV-Cache

攻击者离线使用相同模型,基于目标主题Prompt生成一段完整KV-Cache块(topic_cache)。

步骤2: 启动正常生成并等待替换点

从受害者Prompt开始自回归生成,监控已生成token数,直到达到预设swap_token(例如序列的20%-60%处)。

步骤3: 执行块级覆盖替换

计算替换段长度(swap_percent,如25%-75%最近timestep),在全层(或指定早/晚层)用topic_cache对应部分直接覆盖当前缓存。

步骤4: 继续生成并观察劫持

模型基于篡改缓存继续输出。常见效果:立即/延迟主题偏移、原主题与攻击主题交替、或生成重复崩溃。

3.3 KV-Cache 腐败攻击

image-20251230101806716

KV-Cache腐败攻击原理图

攻击者通过向KV-Cache注入扰动(perturbation),破坏注意力机制的完整性,导致输出偏差、性能下降或幻觉增加。这种攻击视KV-Cache为“内存腐败”类似漏洞,扰动键向量(Key vectors)即可放大影响。

设定攻击场景:在正常生成或RAG任务中,攻击者向KV-Cache的Key向量注入扰动,导致注意力偏差、性能下降或幻觉增加。

步骤1: 选择目标层与时机

确定最脆弱层(通常中层,如LLaMA-2第12层)和扰动应用频率(连续或间歇)。

步骤2: 选择扰动变体

  • MTI-Gaussian:添加高斯噪声(σ=0.1-5.0)
  • MTI-Zeroing:概率置零Key条目
  • MTI-Rotation:施加正交旋转(15°-90°)
  • 可结合梯度优化以最大化目标影响

步骤3: 注入扰动到Key向量

在生成过程中,按选定策略对Key向量应用扰动δ。

步骤4: 观察输出效果

监控下一token分布偏移(KL散度上升)、下游任务性能下降15–30%、或RAG幻觉率增加5%-12%。中层扰动放大效果最显著。

0x04 代码实现

测试为纯CPU环境下完成,基于Python3.8+的Hugging Face Transformers与PyTorch运行124M参数的gpt2模型。

4.1 KV-Cache时序侧信道攻击

实验1:基础缓存时序测量

验证KV-Cache复用是否产生物理上可观测的时间差异。

我们实现了一个多租户LLM服务的 KVServer 类,支持:

  1. 最长前缀匹配 (LPM):实现类似vLLM的Prefix Caching
  2. 精确计时:仅测量Prefill阶段的KV 计算,排除tokenization开销
  3. 缓存管理:LRU淘汰策略

核心实现

@dataclass
class _CacheEnt:
    """KV-Cache 条目"""
    prompt: str
    input_ids: torch.Tensor
    past_kv: Tuple
    ts: float

class KVServer:
    """多租户KV-Cache服务器"""

    def _lpm(self, q_ids: torch.Tensor):
        """Longest Prefix Match - 缓存必须是查询的前缀"""
        best = None
        best_len = 0

        for cached, ent in self._cache.items():
            c_ids = ent.input_ids[0].tolist()
            q = q_ids[0].tolist()

            # 计算共同前缀长度
            mlen = 0
            for i, (a, b) in enumerate(zip(c_ids, q)):
                if a == b:
                    mlen = i + 1
                else:
                    break

            # 缓存有效条件:缓存是查询的前缀(mlen == len(cached))
            if mlen > best_len and mlen == len(c_ids) and len(c_ids) <= len(q):
                best = ent
                best_len = mlen

        return (best, best_len) if best else None

    def process(self, prompt: str, max_new=1, uid="anon", write_cache=True):
        """处理请求,返回详细的时序数据"""
        input_ids = self.tok.encode(prompt, return_tensors="pt")
        t0 = time.perf_counter()

        cache_r = self._lpm(input_ids)

        with torch.no_grad():
            if cache_r:
                # 缓存命中路径:复用past_key_values
                ent, matched = cache_r
                self._hits += 1

                if input_ids.shape[1] > matched:
                    # 部分匹配:计算增量部分
                    delta_ids = input_ids[:, matched:]
                    out = self.model(
                        delta_ids,
                        past_key_values=ent.past_kv,
                        use_cache=True
                    )
                    past_kv = out.past_key_values
                else:
                    # 完全命中:直接复用
                    past_kv = ent.past_kv

                prefill_t = (time.perf_counter() - t0) * 1000
                hit = True
            else:
                # 缓存未命中路径:完整前向传播
                self._miss += 1
                out = self.model(input_ids, use_cache=True)
                past_kv = out.past_key_values
                prefill_t = (time.perf_counter() - t0) * 1000
                hit = False

        # ... 生成阶段与缓存写回

运行结果

可以看到上面第一次请求Prefill用了大约380ms,这是模型执行完整前向传播的时间。对于GPT-2,这意味着要进行12层TransformerBlock的矩阵乘法运算。

然后当二次请求完全相同的时候Prefill仅仅为0.024ms ,几乎就只有内存操作时间。这个原因是因为past_key_values已存在,模型跳过了所有Attention层的Q×KT​计算,仅需简单的张量拼接。

实验2:prompt探测攻击

为了验证攻击者能否通过时序差异识别受害者的Prompt

核心代码:

def experiment_2_exact_match_attack():
    # 步骤1: 受害者缓存敏感Prompt
    victim_prompts = [
        "My secret password is hunter2",
        "My API key is sk-1234567890abcdef",
    ]

    for prompt in victim_prompts:
        server.process(prompt, uid="victim", write_cache=True)

    # 步骤2: 攻击者构造候选列表
    candidates = [
        "My secret password is hunter2",      # ✓ 匹配
        "My secret password is wrong",        # ✗ 不匹配
        "My secret code is hunter2",          # ✗ 不匹配
        "My API key is sk-1234567890abcdef",  # ✓ 匹配
        "My API key is sk-wrong-key",         # ✗ 不匹配
    ]

    # 步骤3: 逐个探测
    discovered = []
    for cand in candidates:
        r = server.process(cand, uid="attacker", write_cache=False)
        t = r['prefill_ms']

        if t < 1.0:  # 阈值判定
            discovered.append((cand, t))

    return discovered

运行结果

image-20251230222154860

我们可以从上面实验结果看到当探测内容与缓存完全一致时,模型无需任何计算,直接返回缓存指针。时间差异非常大。

我们可以从攻击者视角的视角来看到这件事:

1.攻击者构造"密码候选列表"(类似字典攻击),逐个探测。

2.只要有一个候选的响应时间<1ms,攻击者就能推断出被攻击者的完整prompt。

攻击简易流程图如下。

1

4.2 History Swapping攻击

实验目标:在不改变用户可见Prompt的情况下,通过替换推理过程中的past_key_values片段,把模型输出从“受害者话题”劫持到“攻击者话题”。

  • 受害者请求:正常的业务问题(例如“如何制作咖啡”)。
  • 攻击者能力


    • 能在同一推理进程/同一GPU的Worker内“写入或污染”共享的KV-Cache(例如:推理引擎实现了前缀缓存复用、调度/缓存对象复用存在隔离缺陷、或插件/监控/扩展组件可触达缓存对象)。
    • 攻击者提前离线生成一段目标主题的K-Cache。
    • 攻击效果:用户看到的prompt没变,但输出内容发生明显“叙事漂移”。

核心思路

  1. 攻击者用目标主题prompt跑一次Prefill,得到attacker_cache
  2. 受害者开始生成,达到某个swap_at_token时刻。
  3. 在所有层(或关键层)将受害者cache的一段时间步区间(如中间30%-60%)用attacker_cache的片段覆盖。

核心实现:

def gen_with_swap(model, tok, prompt: str, max_tok=30,
                 atk_cache=None, swap_at=2):
    """带缓存替换的生成函数"""
    ids = tok.encode(prompt, return_tensors="pt")
    generated = []

    # Prefill 阶段
    with torch.no_grad():
        out = model(input_ids=ids, use_cache=True)
        past = out.past_key_values
        logits = out.logits[0, -1, :]

    # 逐 token 生成
    for step in range(max_tok):
        tok_id = torch.argmax(logits).item()
        generated.append(tok_id)

        # 关键:在指定步数替换缓存
        if step == swap_at and atk_cache is not None:
            past = _swap_mix(past, atk_cache)

        nxt = torch.tensor([[tok_id]])
        with torch.no_grad():
            out = model(input_ids=nxt, past_key_values=past, use_cache=True)
            past = out.past_key_values
            logits = out.logits[0, -1, :]

    return tok.decode(generated, skip_special_tokens=True)

def _swap_mix(vic_cache, atk_cache):
    """混合策略:保留 10% 受害者前缀,替换中间 85% 为攻击者缓存"""
    from transformers.cache_utils import DynamicCache

    # 提取张量
    if hasattr(vic_cache, "key_cache"):
        v_k = [vic_cache.key_cache[i].clone() for i in range(len(vic_cache.key_cache))]
        v_v = [vic_cache.value_cache[i].clone() for i in range(len(vic_cache.value_cache))]
        a_k = [atk_cache.key_cache[i] for i in range(len(atk_cache.key_cache))]
        a_v = [atk_cache.value_cache[i] for i in range(len(atk_cache.value_cache))]
    else:
        # 兼容 tuple 格式
        v_k = [vic_cache[i][0].clone() for i in range(len(vic_cache))]
        v_v = [vic_cache[i][1].clone() for i in range(len(vic_cache))]
        a_k = [atk_cache[i][0] for i in range(len(atk_cache))]
        a_v = [atk_cache[i][1] for i in range(len(atk_cache))]

    new_cache = DynamicCache()

    for layer in range(len(v_k)):
        vk, vv = v_k[layer], v_v[layer]
        ak, av = a_k[layer], a_v[layer]

        seq_len = vk.shape[2]
        atk_len = ak.shape[2]

        # 替换策略:保留 10%,替换 10%-95%
        start = int(seq_len * 0.1)
        end = int(seq_len * 0.95)
        swap_sz = min(end - start, atk_len)

        nk = vk.clone()
        nv = vv.clone()

        if swap_sz > 0:
            # 关键:切片替换
            nk[:, :, start:start+swap_sz, :] = ak[:, :, :swap_sz, :]
            nv[:, :, start:start+swap_sz, :] = av[:, :, :swap_sz, :]

        new_cache.key_cache.append(nk)
        new_cache.value_cache.append(nv)

    return new_cache

image-20251229201348226

在我们进行swap_at_token 之后,输出出现明显话题漂移,原本应该是咖啡的制作方面的东西,结果话题漂移到了星空上面。

4.3 KV-Cache腐败攻击

实验:Cache Corruption(扰动Key 向量)

实验目标:在生成过程中对KV-Cache的Key张量注入扰动(噪声/置零/旋转),观察注意力机制被破坏后带来的输出质量退化(重复、语义漂移、幻觉倾向上升)。

更贴近实战的场景设定

  • 共享显存/共享推理Worker:攻击者通过越权写入或内存破坏类漏洞(例如缓存指针复用错误、越界写、错误的张量视图复用)影响到其他请求的KV。
  • RAG/Agent场景:缓存腐败会显著增加“把检索内容读错/拼接错”的概率,表现为幻觉或逻辑断裂。

扰动策略

  • corrupt_gaussian:K = K + N(0, σ^2)
  • corrupt_zeroing:以概p 将Key条目置零
  • corrupt_rotation:对Key的embedding子空间做正交旋转(简化实现为对最后维度两两旋转)

核心实现:

#高斯噪声
def corrupt_gaussian(cache, sig=1.0):
    """对 Key 向量添加高斯噪声:K = K + N(0, σ²)"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:  # 中层更敏感
            noise = torch.randn_like(k) * sig
            out.append((k + noise, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

#随机置零
def corrupt_zeroing(cache, p=0.3):
    """以概率 p 将 Key 条目置零"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:
            mask = (torch.rand_like(k) > p).float()
            out.append((k * mask, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

#正交旋转
def corrupt_rotation(cache, deg=45.0):
    """对 Key 的 embedding 子空间做正交旋转"""
    ts = _extract(cache)
    out = []
    mid = len(ts) // 2

    rad = np.radians(deg)
    c, s = np.cos(rad), np.sin(rad)

    for i, (k, v) in enumerate(ts):
        if abs(i - mid) <= 1:
            nk = k.clone()
            d = k.shape[-1]
            # 对最后维度两两旋转
            for j in range(0, d - 1, 2):
                kj = k[:, :, :, j].clone()
                kj1 = k[:, :, :, j + 1].clone()
                nk[:, :, :, j] = c * kj - s * kj1
                nk[:, :, :, j + 1] = s * kj + c * kj1
            out.append((nk, v.clone()))
        else:
            out.append((k.clone(), v.clone()))

    return _rebuild(out)

实验结果如下

image-20251230223603113

可以看到在不同扰动策略下模型输出的内容发生明显变化。

0x05 防御与缓解措施

以下从架构、系统、审计三层总结主流缓解措施,结合最新研究(如SafeKV、KV-Cloak),旨在平衡安全性、性能与部署成本。

5.1 架构层防御

  • 租户级缓存隔离:通过Tenant ID、Session Scope或用户唯一标识符划分KV命名空间,完全禁止跨租户共享。适用于高敏感场景,虽牺牲部分吞吐,但彻底消除侧信道。
  • 选择性共享:仅允许非敏感前缀共享,结合细粒度隐私策略(如基于内容分类)决定复用范围。
  • 缓存生命周期管理:单次请求后自动清除,或设置TTL过期策略,减少驻留时间泄露风险。
  • LPM随机化与分区:在最长前缀匹配中引入随机扰动,或按哈希分区缓存池,打乱命中可预测性。

5.2 系统层防御

  • 噪声注入与延迟模糊化:在缓存命中路径插入±Δt随机延迟,或对时间指标添加噪声,隐藏TTFT/顺序差异(针对时序攻击)。
  • 缓存内容混淆:使用可逆矩阵变换对KV向量加密/混淆,仅授权方可逆转。
  • 扰动检测与完整性校验:实时监控KV向量范数/哈希变化,检测异常扰动(针对腐败攻击)或引入dropout-mask随机化/注意力平滑,减轻操纵影响。
  • 参数与接口限制:禁止外部暴露use_cache、position_ids等敏感参数;结合速率限制(throttling)阻断高频探测请求。
  • 机密计算集成:利用TEE(Trusted Execution Environment)加密KV-Cache内存,防止物理/侧信道访问。

5.3 审计与合规层

  • 缓存审计器:记录每条请求的缓存命中/共享日志,绘制租户-缓存命中矩阵,便于事后追溯。
  • 异常行为监控:基于机器学习检测异常调度模式(如批量相似前缀探测),自动告警或隔离。
  • 合规框架支持:在SOC 2、ISO 27001或GDPR下,强制日志不可篡改,并定期审计共享安全性。

参考资料

https://openreview.net/pdf?id=gUj2fxQcLZ

https://www.ndss-symposium.org/wp-content/uploads/2025-1772-paper.pdf

https://huggingface.co/docs/transformers/en/kv_cache

https://arxiv.org/abs/2312.07104

https://www.arxiv.org/pdf/2510.17098

https://arxiv.org/pdf/2511.12752

https://pub.towardsai.net/lets-build-an-optimizer-for-a-gpt-model-from-scratch-in-pytorch-kv-caching-4d3f1f9516fa

在大模型(LLM)服务极速发展的当下,效率至关重要。为了降低延迟并控制算力成本,主流推理框架广泛引入了先进的缓存机制。然而,这种追求极致速度的设计是否埋下了安全隐患?

本论文是由奇安信技术研究院、中国海洋大学和清华大学联合完成的AI安全研究工作说明了缓存机制如果实现不恰当的话,就会造成安全隐患。论文题目为《Cache Me, Catch You: Cache Related Security Threats in LLM Serving Frameworks》。这项工作由中国海洋大学和奇安信联合培养的硕士研究生吴祥凡在奇安信技术研究院联培期间主导完成,导师为应凌云博士(奇安信星图实验室)和曲海鹏教授(中国海洋大学),其他作者为陈国强(奇安信星图实验室),谷雅聪(清华大学)。这项研究聚焦于大语言模型(LLM)推理服务框架中的安全威胁,深入分析了 KV Cache、多模态缓存及语义缓存 三大核心机制。

1. LLM推理加速背后的隐忧

随着模型参数规模的不断膨胀,推理计算的开销急剧上升。为了优化用户体验,vLLM、SGLang、GPTCache等主流服务框架引入了多种缓存策略,包括前缀缓存(Prefix Cache)、语义缓存(Semantic Cache)和多模态缓存(Multimodal Cache)。

虽然这些机制通过存储中间状态极大地减少了重复计算,但我们的研究发现,现有的缓存实现往往“重效率、轻安全”。非加密哈希函数的滥用、有缺陷的对象序列化以及模糊的语义匹配标准,共同构成了一个全新的、尚未被充分探索的攻击面。与以往关注训练阶段的数据投毒不同,这是一类发生在推理阶段的全新安全威胁。

2. Cache Me, Catch You:首个LLM缓存安全系统性研究

为了揭示这一风险,我们对主流LLM服务框架的缓存实现进行了全面的解构与分析,并提出了六种新颖的攻击向量。这些攻击利用了哈希碰撞和语义模糊匹配的特性,能够在不接触模型权重的情况下,通过污染共享缓存来操纵模型输出。

主要发现与攻击向量:

我们将发现的威胁归纳为两大类:一是面向用户的欺诈攻击,即攻击者利用系统渠道向用户传递恶意信息 ,具体手段包括利用哈希碰撞替换合法提示词以劫持对话逻辑的系统提示词碰撞、针对语义缓存构造高相似度恶意查询诱导错误回答的语义模糊投毒 ,以及在检索增强生成场景下利用文档相似性扩大攻击面的RAG语义投毒 ;二是系统完整性攻击,旨在破坏服务功能或绕过安全审查 ,具体涵盖构造与目标完整前缀碰撞以劫持响应的提示词碰撞劫持 、通过精心构造padding token让恶意代码块对LLM“隐形”以绕过审计的分块碰撞劫持 ,以及利用图像处理忽略元数据(如尺寸)缺陷构造哈希碰撞图片以绕过审核的多模态碰撞 。

细节详解:

以多模态为例,其核心漏洞根源在于当前主流推理框架(如vLLM)在对多模态数据进行序列化时存在严重的逻辑缺陷。具体而言,vLLM默认调用PIL 的 tobytes() 方法来提取图像数据以计算哈希,该方法虽然能获取原始像素字节流,但在vLLM的后续操作中完全忽略了图像宽高等尺寸信息以及调色板等关键元数据。攻击者利用这一特性实施“尺寸伪装”攻击,通过重塑图像维度(例如将 H*W的图像变形为W*H)而不改变像素排列顺序,使得原本违规的图片变成一团毫无意义的噪点,从而生成与原图完全一致的哈希值。此外,攻击者还能利用“调色板模式”漏洞,构造出索引数据相同但颜色定义截然相反的图片对(如黑底白字与白底黑字),由于序列化过程仅读取索引而忽略调色板定义,这两张视觉迥异的图片在系统眼中却拥有相同的“指纹”。

同样的隐患也出现在SGLang框架中,其为了适配张量数值范围将SHA256哈希值进行了取模截断,导致哈希空间被压缩至极易发生碰撞的范围。

下图是我们操纵图片当中的尺寸和PNG当中的P格式的调色盘,实现看上去不同的图片但是hash一致。

3. 实验效果与影响评估

我们在vLLM、SGLang及GPTCache等主流开源框架上进行了实测,证实了这些攻击路径的高可用性与低门槛:攻击者仅需不到1美元的成本即可完成一次投毒 。以针对vLLM的前缀缓存攻击为例,我们在30分钟内便成功搜索到碰撞哈希,实现了100%的缓存命中 。

实验还还原了真实的威胁场景违规图片如何利用多模态缓存缺陷骗过内容审核系统。下图展示一个示意图,成功命中图片之后会复用之前的图片预处理结果,导致生成了错误回复。

4. 防御方案与行业响应

针对发现的漏洞,我们提出了五层防御策略,包括引入随机化哈希(Salting)、采用强加密哈希函数、强制规范化序列化流程、使用更鲁棒的Embedding模型以及增加LLM辅助过滤层。我们的理论分析和实际验证表明,上述的防御方案是可行的、有效的。

我们在第一时间将发现的漏洞通报给了受影响的厂商和社区,包括 vLLM、SGLang、GPTCache、AIBrix、rtp-llm 和 LMDeploy,并分配了 3个 CVE 编号。值得注意的是,vLLM、GPTCache 和 AIBrix 已经采纳了我们提出的缓解措施(如引入随机盐值、规范化图像序列化等)并完成了修复。(在本文发表时,SGLang也反馈采纳了我们的缓解措施。)

5. 讨论与未来展望

我们的研究再次表明,高性能不应成为忽视底层系统安全的理由。本研究证明,即便模型本身无懈可击,外围缓存框架的设计缺陷仍足以瓦解整个系统的信任基石;特别是在云端共享算力场景下,必须实施严格的多租户隔离与键值空间分离以防御跨租户攻击。作为填补推理侧缓存安全空白的先行工作,本研究旨在推动社区正视这一隐蔽威胁,共同构建更稳健的大模型服务基础设施。

更多参考

想了解更多技术细节?欢迎阅读我们的学术论文或访问项目主页:

代码仓库:https://github.com/XingTuLab/Cache_Me_Catch_You

感谢您的阅读,期待能为您的AI安全研究与工程实践带来启发!