[控场AI]
· 4 分钟阅读· 2,306 字

脱敏库泄露私钥:一次对抗性测试的胜利

脱敏库泄露私钥:一次对抗性测试的胜利

脱敏库因信任任意END标记泄露私钥,作者分享四个安全教训。

本文讲述了一个零依赖脱敏库在流式输出中泄露PEM私钥的漏洞。库通过BEGIN PRIVATE KEY标记持有私钥块,但结束判断仅依赖任意带标签的END行。当结束标记被错误写成END CERTIFICATE时,库误以为块已关闭,只掩码头部,base64私钥体明文泄露。测试套件全绿,因为只测试了格式正确的密钥块,未断言畸形块不泄漏。作者转向对抗性属性测试,又发现三个漏洞:流式与整串输出不一致导致引号后IBAN泄露、占位符启发式误判真token、Unicode相似字符绕过ASCII检测器。所有漏洞在0.7.9版本修复,并通过约32万模糊测试分块验证不变式。教训是:通过测试只证明代码做了要求的事,对未要求的事一无所知。

一个零依赖的脱敏库 llm-stream-guardrails 在流式输出中泄露了 PEM 私钥——尽管它的测试套件全是绿的。作者在修复后公开了漏洞细节(GHSA-777v-7mvx-mxgp),并分享了这次经历带来的四个教训。

rss source: I shipped a redaction library, then found it leaking a private key

漏洞本身:信任了错误的结束标记

这个库的职责是在 LLM 输出流到达屏幕或日志之前,剥离其中的秘密信息和 PII。它通过 BEGIN PRIVATE KEY 标记来持有 PEM 私钥并掩码整个块,但判断结束的方式是看是否出现任意带标签的 END 行。

问题就出在这里:当一个私钥的结束标记被错误地写成 END CERTIFICATE 时,库会认为该区域已安全关闭,于是不持有任何内容,仅掩码了头部。base64 编码的私钥体直接以明文流了过去。而这个泄露的体可以重建出可用的私钥。

作者强调,这种错误标记并不罕见——拼接的 PEM 捆绑、截断的捕获、或者让模型把私钥当作证书输出,都会产生这种结果。

PEM(Privacy-Enhanced Mail)是一种常见的密钥和证书编码格式,其结构由 -----BEGIN <label>----- 和 -----END <label>----- 两条边界行包裹 base64 编码的体。PEM 的“标签”并不仅仅是装饰——它是判定该块类型(如 PRIVATE KEY、CERTIFICATE、RSA PRIVATE KEY)的标识。解析器通常依赖标签匹配来确定块的完整范围。如果结束标签与开始标签不一致,严格解析器会拒绝该块,但宽松的解析器可能仅凭出现 END 就认为块已闭合。本漏洞的核心就是解析器把任意 END 行误认为当前块的结束,而不是要求标签精确匹配。实践中,拼接 PEM、复制粘贴错误或模型生成时的标签错误都可能导致这种不一致。因此,安全解析必须绑定开始标签和结束标签,而非依赖任意 END 行。

为什么测试没发现

测试套件断言的是代码设计时想要的行为:格式正确的密钥块会被掩码。它确实做到了。但没有任何测试断言在 畸形块 上 不 发生泄漏。因为没人会为没想到的输入写测试。

作者给出的解法不是增加手写用例,而是从测试示例转向对抗性地测试属性:对于任意输入,策略认为应该掩码的值是否可能到达输出?然后主动寻找能打破这一属性的输入。

属性测试(Property-based Testing)与传统的示例测试(Example-based Testing)不同:后者针对具体输入断言输出,前者则定义适用于所有输入的通用属性(不变式),并自动生成大量随机或边界输入来试图证伪这些属性。在脱敏场景中,一个关键属性是“任何被策略识别为秘密的值都不得以任何形式出现在输出中”。示例测试只能验证设计者预设的合法输入,无法覆盖畸形、截断或对抗性输入;而属性测试通过生成器主动搜索反例,能够发现设计者未曾想到的泄漏路径。模糊测试(Fuzzing)是属性测试的一种常见实现,它通过持续变异输入来探索状态空间。作者提到“约32万个模糊测试分块”,正是利用模糊测试生成大量流式分块来验证不变式,从而暴露了示例测试遗漏的漏洞。

同一轮对抗测试发现的另外三个问题

一旦开始追查属性而非示例,更多问题浮出水面。

引号后的 IBAN。 一个紧跟在 JSON 引号后的有效 IBAN,在整串过滤时被掩码,但在流式处理时被分片释放了——流式输出与整串输出不一致,而库承诺这永远不会发生。

真 token 被当成占位符。 跳过文档占位符的启发式规则,只要值中包含连续四个相同字符就会触发,导致一个碰巧包含四个连续字符的真实凭证被放行。

Unicode 相似字符。 值中的全角数字或不可见分隔符绕过了 ASCII 检测器。

每一个都是单行属性违规,每一个都有绿色的测试套件。

IBAN(国际银行账户号码)是用于跨境银行转账的标准账户标识,通常包含国家代码、校验位和银行账户细节,长度可达34位。在JSON等结构化数据中,IBAN常作为字符串值出现,可能被引号包裹。流式处理时,数据被分成多个分块依次到达,如果脱敏逻辑只在完整字符串上运行,分块边界就可能将IBAN拆开,导致部分片段未被掩码而泄露。Unicode相似字符(如全角数字“1”与ASCII“1”)在视觉上接近,但码点不同;基于ASCII的检测器会忽略全角数字或不可见分隔符(如零宽空格),从而让包含这些字符的真实敏感值绕过检测。这两类问题都属于“流式分块”和“编码变体”带来的属性违规,是脱敏库在真实流式环境中必须防御的对抗性输入。

发布与反思

四个漏洞在 0.7.9 版本中全部修复(npm install llm-stream-guardrails@^0.7.9),每个都配有在旧版本上会失败的回归测试。流式输出与整串输出一致的不变式现在在约 32 万个模糊测试分块中成立,泄露的私钥体不再到达输出。安全公告已公开:GHSA-777v-7mvx-mxgp。

作者最后写道:通过测试只证明代码做了你要求的事,它对你没要求的事一无所知。对于一个脱敏工具——对于任何以「隐藏」为职责的东西——这个差距就是产品的全部。

#脱敏库#私钥泄露#对抗性测试#属性测试#LLM 输出安全#PII 剥离#llm-stream-guardrails
分享:

相关推荐