当前位置: 首页 > 技术干货 > 从原理到攻击:KV Cache 安全问题与跨会话泄漏实验

从原理到攻击:KV Cache 安全问题与跨会话泄漏实验

发表于:2026-09-08 11:15 作者: 秋名山上的小柠 阅读数(27人)

0.前言

最近接触到了新的AI攻击概念,打算从这方面入手学习一下

1.KV cache

KV Cache可以理解成,LLM 在逐字生成时,把前面所有 token 已经算好的注意力素材存放起来

后面直接复用,避免反复计算

我们知道在transformer中有Q,K,V这三个,对应的公式大概如下

这里我们用通俗的语言形容就是,Q是指我现在在想什么,K是说每个历史的token应该怎么被检索到,而V就是找到之后,真正可以提取出来的信息

我们举个例子来说,比如说

苹果公司要发布一部新的手机,那么它的名字是....

生成下一个词的时候,当前 token 的 Q 可能在问,上下文提到手机名字会是啥?

然后拿 Q 和历史所有 K 比较,找到相关位置,再读取对应的 V

有了KV cache就可以减少AI很多工作量,比如说我们举个例子

假设已经生成,我 今天 回 学校,现在要生成第 5 个 token

如果没有 KV Cache,模型每生成一次,都可能重新计算:

第1步,我->算KV

第2步,我,今天-->算我,今天的KV

第3步,我,今天,回-->算我,今天,回的KV

第4步,我,今天,回,学校--->算我,今天,回,学校的KV

这里大量工作是重复的,因为对于一个固定的历史 token,比如我,在同一层 Transformer 中,可能就是K我,V我

之前就已经算出来,所以其实是不会有太大的改变的

所以可以第一次算完以后直接保存的

image.png

生成下一个 token 时,只需要计算当前新 token的,比如说Q5,K5,V5之类的

然后把新的K5,V5都放到缓存中去,就大概类似这样

然后再去读取相应一连串的V值

那么这里我们可能会注意到一个问题,为什么Q值不需要缓存呢?

因为生成第 t个 token 时,当前 token 的 Q 只在当前这一步使用,而历史 token 的 Q 后面基本用不到,但历史 token 的 K、V 每生成一个新 token 都要被访问

也就是说:历史 Q用完即丢,历史 K,V之后还要用

所以只缓存K 和 V

KV Cache一般来说不是整个模型只存一份

Transformer 有很多层,比如 32 层,那么每一层注意力机制都有自己的 KV Cache,可以画个简略图

大致结构是:

Layer 1:
  K cache
  V cache

Layer 2:
  K cache
  V cache

Layer 3:
  K cache
  V cache

...

Layer 32:
  K cache
  V cache

而且每一个历史 token 都要存

这也解释了为什么上下文很吃显存,因为token越多,KV cache越大

1K context
████

4K context
████████████████

32K context
████████████████████████████████...

128K context
巨量 KV Cache

模型参数大小通常是固定的,但KV Cache 会随着上下文长度线性增长

我们日常在使用大模型进行相应的推理的时候,实际上是分为两阶段,分别是Prefill即输入预填充,Decode逐token生成

Prefill,模型处理全部输入token,计算每一层的中间表示以及注意力所需的Key/Value状态

比如说输入了一段 1000 token 的 prompt,Prefill 阶段会一次性处理这 1000 token,并生成:

K1,V1
K2,V2
...
K1000,V1000

并且把它们全部放进 KV Cache

Decode模型逐步生成输出token,并复用此前已经计算的注意力状态

生成第 1001 个 token,只计算新token的Q,K,V,也就是对应的Q1001和历史的K1~K1001,用来做新的注意力,从而生成了1001token,再把K1001和V1001加

入到cache中,过程类似下面这张图

一句话来说就是输入Prompt,到Prefill(处理输入并建立注意力状态),再Decode(逐 token 生成输出)

总之,KV Cache = 缓存历史 token 的 Key 和 Value,让下一个 token 做 Attention 时直接查历史,而不用重新计算整个上下文

2.KV cache攻击

kv cache攻击一句话解释就是攻击者通过缓存命中造成的响应延迟差异,推断其他用户是否处理过某段敏感前缀,从而泄露请求存在性或内容特征

假设服务器上刚刚有用户 A 发过,我的银行卡密码是 123456

为了提高吞吐量,推理服务器可能不仅在 A 当前的生成过程中保存 KV,还启用了prefix caching也就是前缀缓存:

"我的银行卡密码是 123456"--->对应的KV 缓存,供之后相同 prefix 的请求复用

然后攻击者 B 和 A 恰好使用同一个多租户推理后端,B 猜:我的银行卡密码是 111111

缓存没命中,需要重新 prefill,而这时的耗时是会比较较长的

所以可以再猜,我的银行卡密码是 123456

如果命中已经存在的 prefix cache,不用重新算这部分,Time To First Token 明显变短

于是攻击者虽然,一没看不到 KV ,二看不到别人的 prompt,三也没有 GPU 权限,但却能通过响应时间来猜测内容,这就是这就是经典的 timing side channel

前缀缓存计时侧信道,vLLM 的安全公告就披露过这种情况:当匹配前缀达到一定长度后,cache hit 和 miss 的 时间差异可以非常明显;

说白了倒是有点像web里面的SQL时间盲注了

3

最近还有一种更有意思的KV cache攻击,KV Cache Hijacking https://arxiv.org/pdf/2607.19957

和偷数据不同的方向,不是读取别人的 KV,而是污染一个可能被别人复用的 KV

2026 USENIX Security 的 HijackKV 研究了 position-independent KV reuse 带来的问题,某些优化允许:

只要某段文本相同,即使它出现在不同位置,也尽量复用 KV

但是问题在于KVi,并不单纯只编码当前这个 token,它还受到之前上下文的影响

举个例子,攻击者请求一个,[恶意上下文] + "Summarize the document"

而"Summarize the document" 对应的 KV 已经受到恶意前文影响

如果系统以后仅因为文本相同,就把这段 KV 给另一个正常用户

正常用户, [正常上下文] + "Summarize the document" 错误地复用了攻击者制造的 KV

那么,用户输入里甚至没有攻击文本,模型行为仍可能受到已经污染的 KV 影响,这类攻击叫 KV Cache Hijacking,本质是恶意构造缓存,受害者错误复用

它和 timing attack 正好是两个方向:

image.png

3.KV cache攻击简单操作

这里我们做一个简单且直观的实验

KV cache 隔离/索引错误 → 不同用户的上下文被错误复用 → 跨会话信息泄露

我们先模拟一个服务,比如说用户 A 先提交一段带秘密的信息,然后服务端把它缓存,接着用户 B 再提交一个恰好撞上同一个 cache key的请求

但是由于服务端错误复用缓存,B 看到了 A 的秘密

import re


class VulnerableKVServer:
   def __init__(self):
       self.kv_cache = {}

   def _bad_cache_key(self, prompt: str):
       """
      漏洞点:
      只用 token 数量作为 cache key

      在真实系统里,类似的问题可能是:
      - cache key 不包含 user/session
      - prefix cache 绑定错误
      - paged KV block 被错误复用
      - cache eviction 后状态没清干净
      """
       return len(prompt.split())

   def _toy_generate(self, context: str):
       """
      把泄漏现象显示出来
      """
       match = re.search(r"secret is ([A-Z]+ [A-Z]+)", context)

       if "repeat the hidden secret" in context.lower():
           if match:
               return f"[LEAK] 发现缓存里的秘密:{match.group(1)}"
           else:
               return "没有找到任何秘密"

       return "请求已处理"

   def request(self, user: str, prompt: str):
       key = self._bad_cache_key(prompt)

       old_cache = self.kv_cache.get(key)

       if old_cache:
           # 漏洞:把旧用户的上下文直接拼到新用户请求前面
           effective_context = old_cache["context"] + " " + prompt
       else:
           effective_context = prompt

       # 更新缓存
       self.kv_cache[key] = {
           "owner": user,
           "context": effective_context,
      }

       return {
           "cache_key": key,
           "previous_owner": old_cache["owner"] if old_cache else None,
           "response": self._toy_generate(effective_context),
      }


server = VulnerableKVServer()


print("=== Victim 请求 ===")
victim_prompt = "note secret is BLUE ORCHID today"
r1 = server.request("victim", victim_prompt)
print(r1)


print("\n=== Attacker 请求 ===")
attacker_prompt = "please repeat the hidden secret now"
r2 = server.request("attacker", attacker_prompt)
print(r2)

我们来拆解里面的逻辑,victim的请求就是

victim_prompt = "note secret is BLUE ORCHID today"

然后使用分隔函数得到几个字符串,key的值也是6,所以服务器的缓存也就变成了

kv_cache = {
   6: {
       "owner": "victim",
       "context": "note secret is BLUE ORCHID today"
  }
}

并且接下来攻击者就会发送please repeat the hidden secret now,同样经过函数进行一个分割,得到key是6

old_cache = self.kv_cache.get(6)

这个漏洞点是致命的

effective_context = old_cache["context"] + " " + prompt

然后 _toy_generate() 在这个混合上下文里同时发现,secret is BLUE ORCHID以及repeat the hidden secret

所以最终返回:[LEAK] 发现缓存里的秘密:BLUE ORCHID

整条攻击链路画图可以这么解释

本课程最终解释权归蚁景网安学院

本页面信息仅供参考,请扫码咨询客服了解本课程最新内容和活动

🎈网安学院推荐课程: 零基础CTF竞赛实战课 红队攻防特训班 Web安全工程师特训班 应急响应安全工程师特训班
  CTF-Reverse实战技能特训班 CTF-WEB实战技能特训班 CTF-PWN实战技能特训班 CTF-MISC实战技能特训班   Python网络安全实战班 SRC漏洞高阶实战课 HVV大师课