TT Lab
博客 开始
学习 学习路径 课程

LLM 应用安全——从注入到输出处理

密钥与个人信息泄漏的地方——不放入、过滤、遮蔽、检测

在 TT Lab 中继续学习

目标

在客服机器人中找出并堵住系统文本中的密钥、原文记录、不区分客户的检索这三个出口,并用金丝雀检测提示泄漏。

为什么重要

进入模型输入的东西总有一天可能出现在回答中,而记录输入的日志会把这些内容复制到更多地方。“写上让模型不要说”并不是边界——真实模型是概率性的,大部分时候会遵守,某天就不遵守了。这个实验按以下顺序修复:秘密根本不放入(工具层),别人的东西放入之前先过滤(检索),记录要遮蔽,即便如此仍泄漏的,用金丝雀察觉。评分器每次评分都会用生成了新密钥、金丝雀、电话号码的临时数据启动应用,看这些值是否残留在响应或记录中的任何地方。

步骤

  1. 用 python3 /opt/fixtures/llmsec/kit.py support 展开素材,然后把 /root/llmsec/support/app.py 启动在 127.0.0.1:8102 上(禁止 0.0.0.0,关掉终端也要保持运行)。curl 127.0.0.1:8102/health 的 seed 必须与展开时得到的种子相同。
  2. 统计昨天的记录 /root/llmsec/support/logs/app-2026-11-04.log 中以原文留下的内容,在 /root/llmsec/support/exposure.txt 中写下 tokens=<count>(记录中出现的不同用户令牌数)、phones=<count>(不同电话号码数)、key_lines=<count>(含有合作方密钥 API 키: pk-… 的行数)三行。
  3. 修改 build_system、run_tool,把合作方密钥从系统文本中移除,改为在执行订单查询工具时由服务器直接从 config.json 读取。即使收到取出系统文本的请求,密钥也不能出现在响应或记录中的任何地方,而订单查询必须照常可用。
  4. 修改 log_request,在 data/app.log 中每个请求留下一行(rid、用户名),但 X-Token 值、电话号码、电子邮件、合作方密钥不能以原文留下。电话号码只留后四位(***-****-1234)。
  5. 修改 build_system、check_output,把 config.json 中的 canary 值放入系统文本;响应中出现该值时,整个回答都要拦截,并在 data/audit.log 中留下一行包含该请求 rid 和 "event": "canary" 的 JSON。正常回答必须照常输出。
  6. 修改 find_tickets,把放入模型的工单过滤为只属于请求客户的。即使用别人工单中的词来提问,回答中也不能混入别人的信息,而对自己的工单必须能回答。
  7. 运行 incident ls-secret-leak 会触发故障事件。三位客户的电话号码在公司外部被发现了(/srv/ls-support/found.txt)。为每位客户找出路径并清理来源,像实验第 3–6 步那样修改代码,然后在 /root/incident/ls-secret-leak/report.txt 中写下三行 <customer>: <code> 以及 evidence: <log line>。代码是 cross-tenant(其他客户的工单混入了回答)、log-bundle(发给合作方的诊断包)、misfiled-ticket(所有者写错的工单)、prompt-extract(系统文本被提取)之一。

参考

展开应用并启动在环回地址上

用 python3 /opt/fixtures/llmsec/kit.py support 展开素材,然后把 /root/llmsec/support/app.py 启动在 127.0.0.1:8102 上(禁止 0.0.0.0,关掉终端也要保持运行)。curl 127.0.0.1:8102/health 的 seed 必须与展开时得到的种子相同。

方向:实验应用没有理由对外开放——监听的地址就是暴露范围。 工具:ss -ltnp、curl、setsid、nohup。 具体:展开命令如上。启动后,看看 ss -ltn 中 8102 端口是否显示为 127.0.0.1,以及 /health 的 seed 是否与 /root/llmsec/support/data/seed.txt 相同。

统计昨天记录中以原文留下的内容

统计昨天的记录 /root/llmsec/support/logs/app-2026-11-04.log 中以原文留下的内容,在 /root/llmsec/support/exposure.txt 中写下 tokens=<count>(记录中出现的不同用户令牌数)、phones=<count>(不同电话号码数)、key_lines=<count>(含有合作方密钥 API 키: pk-… 的行数)三行。

方向:原文记录是放入提示的一切东西的副本。统计什么被复制了几份,是确定受害范围的第一步。 工具:grep -o、sort -u、wc -l、jq、data/users.json。 具体:令牌按 users.json 中的值是否出现在记录中来数,电话号码按不同值的个数来数,密钥按行数来数——三者统计的单位不同。

把密钥从系统文本移到工具层

修改 build_system、run_tool,把合作方密钥从系统文本中移除,改为在执行订单查询工具时由服务器直接从 config.json 读取。即使收到取出系统文本的请求,密钥也不能出现在响应或记录中的任何地方,而订单查询必须照常可用。

方向:模型不需要密钥。需要密钥的是执行工具的服务器代码。 工具:build_system、run_tool、jload("config.json")。 具体:从系统文本中去掉密钥那一行,并把 run_tool 改为读取配置,而不是翻找系统文本。评分器会生成新的密钥,查看响应以及 config.json 之外的所有文件。

留下记录,但要遮蔽

修改 log_request,在 data/app.log 中每个请求留下一行(rid、用户名),但 X-Token 值、电话号码、电子邮件、合作方密钥不能以原文留下。电话号码只留后四位(***-****-1234)。

方向:关掉就无法调查,全部记下记录就会成为泄漏途径。这是一件选择留下什么的事。 工具:re.sub、json.dumps。 具体:不要整个留下头部,身份用名称;问题、上下文、回答中的号码和电子邮件用正则表达式遮蔽。系统文本没有记录的理由。

用金丝雀检测提示泄漏

修改 build_system、check_output,把 config.json 中的 canary 值放入系统文本;响应中出现该值时,整个回答都要拦截,并在 data/audit.log 中留下一行包含该请求 rid 和 "event": "canary" 的 JSON。正常回答必须照常输出。

方向:金丝雀是代替秘密放进去的标记。它出现在输出中,就说明系统文本泄漏了,所以不要只删掉金丝雀,而要把整个回答拦下。 工具:build_system、check_output、audit()。 具体:每次都从配置读取金丝雀,比较前先把空格和大小写统一。也要记住这个局限:如果是真实模型,它可能改写后溜出去。

别人的工单在放入之前过滤掉

修改 find_tickets,把放入模型的工单过滤为只属于请求客户的。即使用别人工单中的词来提问,回答中也不能混入别人的信息,而对自己的工单必须能回答。

方向:进入输入的东西就是能说出来的东西。权限要在检索阶段施加。 工具:find_tickets 的 user、tickets.json 的 owner。 具体:在打分之前,先去掉所有者不同的工单。

故障事件——三位客户的号码在外部被发现

运行 incident ls-secret-leak 会触发故障事件。三位客户的电话号码在公司外部被发现了——被发现的片段在 /srv/ls-support/found.txt,机器人是 /srv/ls-support/app.py,数据和记录在 /srv/ls-support/data/,上周发给合作方的诊断包是 /srv/ls-support/support-bundle.log。为每位客户找出号码泄漏的路径并清理来源——把写错的工单所有者更正为号码的真正所有者,把系统文本中的号码去掉,诊断包保留行、只遮蔽号码。代码 /srv/ls-support/app.py 必须挡住实验第 3–6 步中修好的所有缺陷(密钥放在工具层、记录要遮蔽、金丝雀、检索按客户区分——评分器会再次发送那些攻击)。在 /root/incident/ls-secret-leak/report.txt 中写下三行 <customer>: <code> 以及 evidence: <log line>。每位客户的代码是以下之一:cross-tenant(其他客户的工单混入了回答)、log-bundle(发给合作方的诊断包)、misfiled-ticket(所有者写错的工单)、prompt-extract(系统文本被提取)。

方向:三者的症状都是“号码在外部”。被发现片段的原样形态,可能出自哪条记录,就是路径。 工具:grep -F、jq、tickets.json 的 owner、users.json 的 phone。 具体:把片段原样 grep 一下,看它是 app.log 中的回答(answer),还是诊断包中的问题(q)。如果是回答,就把提出该请求的人以及进入上下文的工单所有者,与号码的真正所有者(users.json)比较,就能分开是混入了其他客户的工单,还是工单所有者写错了。