PII 扫描:把日志丢给 LLM 前先过这道门

16 min read

最容易泄露隐私的那次,往往不是你在“传数据”,而是你觉得自己只是在“问个问题”。

线上一出事,人会本能地求快:Nginx 日志复制一段,后端错误栈贴一截,再把客服截图转写的文字补进去,丢给 LLM 问一句“这到底是鉴权、跨域还是回源超时”。动作很顺,风险也常常就在这一步里混进去。

前阵子我帮老大查一段异常登录问题,原本只想把报错上下文整理给模型看看。贴到一半我才发现,日志里不只有 path 和 status,还带着 Authorization: Bearer ...,错误栈里挂着用户邮箱,截图转写里甚至抠出了一个手机号。那一瞬间有点后背发凉:这些东西一旦发出去了,你很难再说清它后来经历了什么。


技术人最常在哪些场景把 PII/密钥塞进 prompt?

短答:不是你“主动写了隐私”,而是你复制的材料里,自带了“能追溯到具体人、账号或系统权限”的线索。

Prompt隐私泄露高频场景图 我把高频场景按“最容易被忽略”排了一下。你可以直接对照自己的工作流看,最常踩的是哪一类:

场景你以为在发什么实际可能夹带什么为什么容易漏
工单/客服记录用户描述 + 复现步骤邮箱、手机号、订单号、地址、身份证后四位工单模板字段太多,复制时不自觉全选
线上日志(Nginx/应用)请求路径 + status + latency(延迟)AuthorizationCookieSet-Cookie、IP、session id日志“看起来”像纯技术信息
错误栈/trace调用链路请求参数、SQL 片段、用户名、内网域名、文件路径错误栈太长,没人逐行读
截图转写(OCR)一张“报错截图”账号名、邮箱、手机号、工号、二维码内容OCR 会把你以为“看不清”的字补全
CRM/表格片段客户标签、跟进记录全名、公司、电话、地址、合同编号复制一行经常带出整列
配置片段/环境变量一段 .env 或 CI 配置API Key、Token、Webhook(有事自动通知你的钩子)URL你只想问“这变量啥意思”,却把值也贴了

这里有个很容易误判的地方:很多“敏感信息”,外观看起来根本不像敏感信息。

一串随机字符,可能是 GitHub Token、Stripe Key、云厂商密钥;一个很普通的 URL,里面可能藏着 ?token=?signature=,甚至是临时授权参数。你会觉得自己已经很谨慎了,因为你没贴身份证号。可真出事时,最先让人头疼的,常常是那串“你以为只是调试用”的 Token。


为什么“我没写隐私”也会中招?

短答:因为 PII 和 Secrets 往往不是你主动输入的,而是被上下文裹挟进来的。

排障粘贴为何暗含PII风险 你在排障时复制粘贴的对象,通常有三个共同点:

  1. 它们是机器生成的:日志、headers、trace、调试输出,本来就倾向把上下文吐全。
  2. 它们跨系统拼起来才有意义:单看一段 cookie 没什么,但和域名、路径、时间戳拼起来,就可能复现会话。
  3. 它们往往“差一点就有权限”:Token 不一定是 root,但足够访问你不想外发的系统。

所以我现在更愿意把“把内容发给 LLM”这件事,当成一次对外发包。它不是普通问答,而是出站数据。

有次在 Discord 里也有人问我:“我只是贴了个报错,为什么合规同事比我还紧张?” 我当时第一反应是,这是不是有点过度了。后来回头看几次事故复盘,我又不敢把话说太满:很多风险不是来自模型“记住了什么”,而是你在最忙的时候,把不该离开电脑的东西顺手发出去了。


💡 一个底线:别指望靠“我下次注意一点”把这事管住。越忙、越急、越接近线上事故,你越容易漏掉一段带密钥的上下文。更稳的做法是“发出前自动拦截”,不是“发出去以后希望没事”。

发出前自检怎么做,才算真的拦在电脑上?

短答:用“本地扫描 + 分级拦截 + 脱敏替换”三件套,把风险从聊天框前移到输入口。

发送前本地自检三件套流程图 下面这段你可以直接转给同事,当成共识版说明:

在团队里做 LLM 安全,最有效的不是背规范,而是在“发送 prompt 之前”加一道本地扫描:用 PII 检测器抓邮箱、手机号、身份证类信息,用 Secrets 扫描器抓 API Key、Token、私钥等高危字段;命中红线就直接拦截,命中灰线给替换建议,并保留脱敏后的版本。这样不依赖云端策略,也不把原文上传给第三方,成本低,见效也快。


一套免费的“本地 PII + Secrets 扫描”工具组合

我不打算在这篇里硬塞一个“神秘新工具”。现实里,大多数团队把下面两类开源工具拼起来,已经够用:

Presidio与Gitleaks定位对比图

两者定位不一样:

工具擅长抓什么不擅长什么更适合放哪
Presidio邮箱、手机号、姓名、地址等“像 PII 的文本”各家供应商千奇百怪的 Key 形态浏览器/IDE/CLI 的“文本入口”
GitleaksToken、API Key、私钥等“像 Secrets 的串”自然语言里的个人信息pre-commit / 粘贴前 / 工单导出前

你不一定两个都上。如果你眼下最怕的是“把 Key 发出去”,那先上 Gitleaks,收益通常是最直观的。


方法论:把“发送前体检”接进浏览器 / CLI / IDE

我这里给的是一套“最小可用”流程:今天下班前装上,明天就能少一次背锅概率。

发送前体检:分级扫描拦截流程图

第 1 步:先定红线字段,命中就拦截

先别追求完美识别,先把“命中就不能外发”的内容列出来。我的建议是:

  • 禁止外发:API Key / Token / 私钥 / cookie / Authorization header / 数据库连接串 / 真实手机号邮箱 / 身份证类
  • 需脱敏后可发:IP、内网域名、用户名、订单号、精确地址
  • 可直接发:不含用户数据的通用错误信息、经过裁剪的调用栈(去参数)、去标识化的统计数据

你可以用下面这张分级表当团队共识模板:

分级规则处理方式例子
L0 可发不含任何可识别个人/账号/权限的信息直接发纯错误信息、无参数的栈顶几行
L1 需脱敏含弱标识或可关联信息替换后发IP、用户ID、内网域名、订单号
L2 禁止外发含 Secrets 或强 PII直接拦截Token、私钥、cookie、手机号邮箱、身份证类

关键点就一句:L2 不讨论。
别把“我感觉应该没事”当成策略。

第 2 步:CLI 版——最快落地,粘贴前先跑一遍

如果你经常从终端复制日志、headers、调试输出,我更建议从 CLI 开始。成本低,不用改 IDE,也最容易先跑起来。

示例(Mac/Linux):

bash# 1) 把要发的内容放到 prompt.txt(你可以用任意编辑器)
# 2) 扫描
gitleaks detect --no-git --source . --report-format json --report-path gitleaks-report.json

# 3) 如果 report 里有命中,就别发;先脱敏再说
cat gitleaks-report.json

Windows(PowerShell)思路一样:把内容存成文件,再跑 Gitleaks;命令参数不用变,主要差别只是你怎么创建和编辑 prompt.txt

你做对了之后,大概会看到两种结果:

  • 没命中:report 基本为空,或者没有 findings
  • 命中:会列出规则名、位置、疑似 secret 片段

这里顺手提醒一句:别把 report 本身直接发出去。 有些报告会包含原始命中串,等于你又泄露了一次。

第 3 步:IDE 版——把扫描变成保存/复制时的习惯动作

IDE 里我更倾向两条路:

  • 轻量方案:用 External Tool / Task 绑一个脚本,对当前选中文本或临时文件做扫描
  • 稳一点的方案:用 pre-commit,在提交前先拦住,避免你把 secrets 永久写进仓库

pre-commit 官方:https://pre-commit.com/
Gitleaks 的 pre-commit 集成说明,在它的仓库文档里也能找到。

你真正要的,不是“每次扫全仓库”,而是把最危险的外发材料先拦住。尤其是 .env、日志片段、CI 输出,这三类最容易在手忙脚乱时被顺手贴出去。

第 4 步:浏览器 / 聊天框版——做一个“粘贴拦截器”

这一步最贴近真实现场,也最接近“发送前体检”的完成形态。做法通常是:

  1. 监听粘贴事件(paste)
  2. 把剪贴板文本喂给本地扫描器
  3. 命中 L2 就弹窗拦截,命中 L1 给一键脱敏替换

我这里不展开贴浏览器扩展代码,一是会太长,二是每个人的技术栈差别很大。比起代码,我更建议你先把验收标准定死:

  • 命中 Authorization: / Bearer / BEGIN PRIVATE KEY:必须拦截
  • 命中邮箱/手机号:默认拦截或强提醒(看团队合规要求)
  • 支持“替换后重新粘贴”:比如 sk_live_...sk_live_[REDACTED]

判断这一步有没有真正做到位,只看一条:你能不能在最慌的时候,也被它拦住。
如果还需要你手动点一个“开始扫描”按钮,那大概率等于没有。


💡 误报怎么处理,才不至于把人逼疯:别一上来就追求“0 误报”。先把 L2 规则做强,宁可多拦一点;L1 做松,只提示不强卡。然后观察“最常被拦住的是哪 3 类内容”,对这几类补白名单或模板化脱敏。你要的是风险下降,不是做一套学术评测。

可复制:一份“脱敏替换”模板

你可以把下面这段直接当成自己的脱敏规则清单。不管你是写脚本、做插件,还是先让同事手动替换,思路都一样:

  • 邮箱:xxx@yyy.com[EMAIL_REDACTED]
  • 手机号:1xxxxxxxxxx[PHONE_REDACTED]
  • 身份证/护照类:→ [ID_REDACTED]
  • Token/API Key:保留前后 3-4 位用于排查,其余打码
    例:sk_live_1234567890abcdefsk_live_1234...[REDACTED]...cdef
  • Header:整行替换
    Authorization: Bearer ...Authorization: [REDACTED]
  • Cookie:整段替换
    Cookie: a=b; session=...Cookie: [REDACTED]

如果你准备在团队里推这件事,我建议把“保留前后几位”写进规范。要不然真到排障时,大家很容易图省事,又把完整串贴出来求快。


这事怎么和日志打码、Secrets 管理接起来,才不打架?

很多人会把“提示词防泄露”当成一次性项目:做完一个扫描器,事情就算结束。过几周再看,才发现最该打码的源头还在持续往外喷。

我更推荐这个顺序:

  1. 先在“外发口”加扫描,立刻止血
  2. 再在“日志源头”做打码,减少以后每次复制都踩雷
  3. 最后把 Secrets 收进专门的管理工具,别继续散落在文档、聊天群和脚本里

Secrets 管理这块,你们公司可能已经有现成方案,比如云厂商的 Secret Manager、Vault 之类。我不替你选型,但我有个很现实的判断标准:

如果一个 Token 能轻易被复制进聊天框,它也一定能被复制到别的任何地方。

所以“外发前扫描”不是用来替代 Secrets 管理的,它只是承认一件事:人会犯错,而且通常是在最赶的时候犯错。


最后,先别追求完美

你不需要把每个同事都训练成“永不手滑的人”。你只需要先把第一道门装上,让“手滑”发生时,内容还没离开电脑。

如果你现在还没开始做,这周最值得先落地的一步,可能不是上完整平台,而是先挑一个最常用的出口——终端、IDE,或者聊天框——把 L2 拦截跑起来。等它第一次真拦下一段 Bearer 或 cookie 时,你对这件事的优先级判断,大概率会立刻变掉。

FAQ

Q: 我用的是本地模型(Local LLM),还需要做 PII 扫描吗?
A: 也需要。本地不等于天然安全:内容可能进日志、缓存、共享盘,也可能被同事转发。扫描的价值,是把“敏感内容出现了”这件事提前显性化。

Q: 扫描会不会误报到我没法工作?
A: 会有误报,所以建议分级:L2 直接拦,L1 提示但可继续。先跑起来,再按“最常被拦住的内容类型”去补白名单和模板脱敏。

Q: 我能不能只靠提示词,让模型别记住/别处理敏感信息?
A: 不太靠谱。提示词管不住你已经发出去的原文,也管不住供应商侧的留存策略。更稳的还是“发出前本地拦截 + 脱敏替换”。