数据脱敏相关调研
一套真正跑过 5.4 GB 真实 coding-agent 会话的凭证清除方案——不是纸面方案。它解决了一个很具体的问题,也明确没有解决另外两个更大的问题。两部分同样重要。
数据来自 API 中转层录制的真实 coding-agent 会话。真实流量里不可避免地混着活体凭证——API key、云平台密钥、私钥文件内容——而且它们会出现在任何位置:消息正文、工具调用参数、工具返回、用户随手粘贴的代码块。
目标很明确:清除所有可用凭证,同时对数据做字节级的最小改动。训练数据的价值在于真实性,所以消息文本、代码、文件路径、项目名一律保持原样,只有凭证子串被换成带类型的占位符。工具是一个约 130 行、无第三方依赖的 Python 脚本。
在字节层替换,不解析 JSON
脚本逐行处理 JSONL,直接对原始字符串做正则替换,不做「反序列化 → 改 → 重新序列化」。这个选择贯穿了后面所有设计,理由有三个:
json.loads 校验,任何一次替换只要破坏了 JSON 结构就立即中止,杜绝静默损坏。宁可整批失败,不要悄悄产出坏数据。代价是:正则必须自己尊重 JSON 的转义规则。这个代价在第二层里变成了一个真实的坑,见下文的护栏。
八条类型化正则
按顺序应用,更特殊的模式必须排在前面——sk-ant- 要先于 sk-,否则 Anthropic 的 key 会被误标成 OpenAI 的。
| 类别 | 匹配要点 | 占位符 |
|---|---|---|
| private_key | 以 -----BEGIN … PRIVATE KEY----- 起始,吞掉后续 base64 体;END 标记可选 | <REDACTED_PRIVATE_KEY> |
| anthropic_key | sk-ant- + 20 位以上 | <REDACTED_ANTHROPIC_KEY> |
| openai_key | sk-(负向前瞻排除 sk-ant-)+ 20 位以上 | <REDACTED_OPENAI_KEY> |
| github_token | ghp_ / gho_ / ghu_ / ghs_ / ghr_ 及 github_pat_ | <REDACTED_GITHUB_TOKEN> |
| aws_key | AKIA + 16 位 | <REDACTED_AWS_KEY> |
| gcp_key | AIza + 35 位 | <REDACTED_GCP_KEY> |
| slack_token | xox[baprs]- | <REDACTED_SLACK_TOKEN> |
| bearer | Bearer 后 30 位以上的 token(保留 Bearer 字样) | Bearer <REDACTED_TOKEN> |
私钥规则刻意做成「从头吃到 base64 体」,而不是「从头匹配到尾标记」。因为对话里出现的私钥经常是粘贴过来的半截——没有 -----END-----——但残体依然是可用的密钥材料,必须照删。要求匹配到结束标记的规则会把它们全部漏掉。
被源码「打散」的私钥
最麻烦的情况是私钥以代码形态出现:被宿主语言包装,比如字符串拼接、转义的换行、heredoc。此时私钥被切成许多短 base64 片段,任何「整块匹配」的正则都必然漏。
// 真实数据里长这样 —— 密钥被 " + " 切成了碎片 // 整块正则匹配不到任何一段足够长的 base64 const PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" + ← 只有头是完整的 "MIIEowIBAAKCAQEAxK9fQ2" + ← 22 字符,太短 "vN8mR4tYuI0pAsDfGhJkLz" + ← 22 字符,太短 "XcVbNmQwErTyUiOpAsDfGh\n" + ← 22 字符,太短 "-----END RSA PRIVATE KEY-----";
对策是放弃匹配「块的形状」,改为锚定加清扫:
base64 的匹配带一个负向后顾断言 (?<!\\),禁止从 JSON 转义序列的字母上起步。
因为 \n、\uXXXX 里的 n、u 本身就是合法的 base64 字符。不加这道护栏,正则会从转义序列的字母开始吃,吃掉那个字母、留下一个悬空的反斜杠,整条 JSON 记录就此损坏。
替换统计与验证
| 类别 | 替换次数 | 说明 |
|---|---|---|
| openai_key | 9,358 | 占了绝大多数 |
| key_material | 369 | 第二层「锚定–求证–清扫」的战果 |
| bearer | 45 | |
| github_token | 41 | |
| aws_key | 20 | |
| gcp_key | 2 | |
| anthropic_key | 1 | |
| private_key | 1 | 整块完整的私钥只有一个——其余全是被打散的 |
| slack_token | 0 |
计数按出现次数:同一把密钥每出现一次记一次,所以次数远大于不同密钥的数量。796 条记录(4.3%)含凭证。
① 对输出文件重新扫描全部凭证模式,零残留;② 全部 18,380 条记录仍能通过 JSON 解析(逐行校验在替换时同步执行,坏一条即中止导出)。
另有一道与脚本无关的前置防线:中转层导出时从不携带调用方元数据(IP、token 哈希等),它们根本不进数据集。
正则迭代了三轮才收敛
两个坑都在私钥上,而且都指向同一个方向:
- 截断私钥——早期规则要求匹配到 -----END-----,漏掉了对话里粘贴的半截密钥。修正:END 标记改为可选,匹配「头 + base64 体」。
- 源码拼接私钥——被 " + " 打散的片段短于任何合理的整块阈值。修正:引入第二层的「锚定 + 求证 + 清扫」。
对「密钥出现在哪个语境」建模——header 锚点 + 邻近窗口——更稳。
它没有解决什么
原文档第 6 节把边界写得很清楚:脱的是各类活体凭证,无论出现在文本、代码还是工具输出里;不脱的是文件路径、项目名、用户粘贴的其他内容——全部原样保留。
这个诚实的边界声明,恰好点出了一个更大的问题:日常说的「脱敏」其实混着三件完全不同的事,而这套方案只覆盖第一件。
因为对外说「我们做了脱敏」时,对方听到的往往是第三层。而买方真正会追问的也是第三层——他们关心的是授权链,不是密钥。
把三件事混成一句「已脱敏」,会让「安全上没问题」被误读成「法律上可以卖了」。这两件事之间隔着的东西,一行正则也补不上。
市场上买方怎么描述同一件事
把这套方案和买方地图里 Mercor Data 的公开说法放在一起,差距很说明问题。
| 维度 | 本方案 | Mercor Data 的公开描述 |
|---|---|---|
| 目标 | 清除活体凭证 | 不可逆遮蔽 PII、PHI、BII(个人 / 健康 / 商业身份信息) |
| 手段 | 类型化正则 + 语境锚定窗口 | 「确定性模式匹配 + 生成式 AI」 |
| 对原文的改动 | 字节级最小改动,替换点之外完全一致 | 未公开 |
| 在流程中的位置 | 数据集导出后的后处理 | 产品流程的内建一环,且前置了只读 OAuth 授权与企业签约 |
| 授权链 | 不在处理范围内 | 由企业签约与 OAuth 授权在抽取之前就解决 |
差别不在正则写得好不好——本方案在纯技术层面做得更细致,也更透明。
差别在顺序:Mercor 把授权放在采集之前,所以它的匿名化只需要解决安全问题;而对一份已经录好的存量数据做后处理,无论做得多干净,都补不回一个当初不存在的授权。这也正是买方地图里那条链的含义——市场付费的形态是「先拿到同意,再采集」,不是「先采集,再脱敏」。