发布前脱敏:规则 + AI 双层防护

发布前脱敏:规则 + AI 双层防护
我有个习惯:写技术笔记时会把真实的命令、配置、密钥都记下来。
这样下次用的时候能直接复制,不用再去找。问题是,这些笔记我有时候想发到博客上分享。
手动脱敏太累了——要把每个 IP 改成 x.x.x.x,把每个 API Key 改成 sk-xxx,还要检查有没有漏掉的密码。漏一个就是安全事故。
后来我写了个自动脱敏模块:发布时自动扫描,规则检测常见模式,AI 兜底漏网之鱼。
一句话介绍
sensitive-sanitizer 是一个发布前脱敏模块,用正则规则处理固定模式(IP、API Key、Token),用 AI 捕获规则难以覆盖的敏感片段。
效果展示
发布文章时的输出:
发布: ~/Notes/VPS配置.md
标题: VPS 配置笔记
Slug: vps-config
脱敏完成: 规则 5 处, AI 3 处
发布成功: https://s.chen.rs/vps-config
原文:
服务器 IP: <REDACTED_IP>
SSH 密码: <REDACTED_PASSWORD>
API Key: sk-proj-abc123def456...
发布后:
服务器 IP: <REDACTED_IP>
SSH 密码: <REDACTED_PASSWORD>
API Key: <REDACTED_API_KEY>
本地文件不变,只有发布内容被脱敏。
快速开始
1. 安装依赖
cd ~/mom/tools
bun install
2. 配置 AI(可选但推荐)
export AI_API_KEY="sk-..."
# 或
export OPENAI_API_KEY="sk-..."
没有 AI Key 也能用,只是少了 AI 兜底层。
3. 在代码中使用
import { sanitizeSensitiveContent } from "./lib/sensitive-sanitizer";
const result = await sanitizeSensitiveContent(markdown, {
useAi: true, // 是否启用 AI 脱敏
});
console.log(result.content); // 脱敏后的内容
console.log(result.replacements); // 替换记录
4. 通过 CLI 发布(自动脱敏)
# 默认启用规则 + AI 脱敏
bun tools/share.ts article.md
# 只用规则脱敏(关闭 AI)
bun tools/share.ts article.md --no-sanitize-ai
# 完全关闭脱敏(不推荐)
bun tools/share.ts article.md --no-sanitize
核心架构
输入 Markdown
│
▼
┌─────────────────┐
│ 规则脱敏层 │ ← 9 条正则规则,处理固定模式
│ (确定性) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ AI 脱敏层 │ ← LLM 识别规则漏掉的敏感片段
│ (语义理解) │
└────────┬────────┘
│
▼
输出脱敏内容
为什么要两层?
- 规则层:快、准、稳。API Key 长什么样是固定的,正则能 100% 匹配。
- AI 层:灵活。"密码是 abc123" 这种自然语言描述,规则很难覆盖,AI 能理解。
规则层详解
目前实现了 9 条规则:
| 规则 | 匹配模式 | 替换为 |
|---|---|---|
| public-ipv4 | <REDACTED_IP> |
<REDACTED_IP> |
| ssh-fingerprint | SHA256:MkYY9q... |
SHA256:<REDACTED_FINGERPRINT> |
| authorization-bearer | Authorization: Bearer xxx |
Authorization: Bearer <REDACTED_TOKEN> |
| share-api-key | sk-proj-abc123... |
<REDACTED_API_KEY> |
| hysteria2-uri-auth | hysteria2://<REDACTED_AUTH>@... |
hysteria2://<REDACTED_AUTH>@... |
| base64-decoded-comment | base64 -d) # secret |
base64 -d) # <REDACTED_COMMENT_SECRET> |
| cloudflare-tunnel-id | tunnel: 74db0e95-... |
tunnel: <TUNNEL_ID> |
| cloudflare-credentials-file-id | .../74db0e95-....json |
.../<TUNNEL_ID>.json |
| url-query-token | ?token=abc123... |
?token=<REDACTED_TOKEN> |
关键设计:保留结构,只替换敏感值。
比如 hysteria2://<REDACTED_AUTH>@<REDACTED_IP>:443,替换后是 hysteria2://<REDACTED_AUTH>@<REDACTED_IP>:443,保持了 URI 格式,读者能理解这是什么。
豁免机制
有些内容看起来像敏感信息,但其实是占位符:
function shouldKeepToken(token: string): boolean {
return (
token.startsWith("$") || // $API_KEY
token.startsWith("<") || // <YOUR_TOKEN>
token.includes("xxx") || // sk-xxx
token.includes("...") || // sk-...
token.toLowerCase().includes("redacted") // 已脱敏
);
}
127.0.0.1 和 0.0.0.0 这种本地地址也会保留:
function shouldKeepIp(ip: string): boolean {
return ip === "127.0.0.1" || ip === "0.0.0.0" || ip === "100.100.100.100";
}
AI 层详解
规则能覆盖固定模式,但自然语言描述的敏感信息很难用正则匹配:
登录密码是 <REDACTED_PASSWORD>
服务器用户名 admin,密码同上
联系我的微信:<REDACTED_WECHAT>
这时候 AI 就派上用场了。
Prompt 设计
const { data } = await chatJson<AiRedactionResponse>(
[
{
role: "system",
content: "你是安全脱敏助手。请找出 markdown 中仍可能泄露敏感信息的短片段,并给出替换。不要改写非敏感内容。",
},
{
role: "user",
content: `请只返回 JSON,格式如下:
{"replacements":[{"from":"原文片段","to":"<REDACTED_XXX>","reason":"原因"}]}
要求:
1) from 必须是原文中存在的精确片段,且尽量短
2) to 必须是 <REDACTED_XXX> 形式
3) 不要处理占位符(如 $API_KEY、<TOKEN>、xxx)
4) 最多返回 12 条
待处理文本:
${input}`,
},
],
{ task: "extract", effort: "balanced", maxTokens: 1200 }
);
关键约束:
- 精确匹配:
from必须在原文中存在,避免 AI 幻觉 - 格式统一:
to必须是<REDACTED_XXX>形式 - 跳过占位符:避免把示例代码也脱敏了
- 数量限制:最多 12 条,防止过度脱敏
校验机制
AI 的输出不能直接信任,需要校验:
function validateAiReplacement(from: string, to: string): boolean {
if (!from || !to) return false;
if (from.length < 4 || from.length > 120) return false; // 长度合理
if (to.length < 10 || to.length > 80) return false;
if (!to.startsWith("<REDACTED_")) return false; // 格式正确
if (!to.endsWith(">")) return false;
if (from.includes("\n")) return false; // 单行
if (shouldKeepToken(from)) return false; // 不是占位符
return true;
}
然后用文本替换而非正则,确保精确匹配:
function replaceAllByText(content: string, from: string, to: string) {
if (!content.includes(from)) {
return { content, count: 0 }; // from 不存在就跳过
}
const parts = content.split(from);
return {
content: parts.join(to),
count: parts.length - 1,
};
}
安全兜底
AI 可能失败(超时、API 错误、返回格式错误),所以有多层兜底:
// 没有 API Key
if (!canUseAi()) {
return { content: input, replacements: [], skippedReason: "未检测到 AI_API_KEY" };
}
// 内容太长
if (input.length > 30000) {
return { content: input, replacements: [], skippedReason: "内容超过 30000 字符" };
}
// API 调用失败
try {
// ... AI 调用
} catch (error) {
return { content: input, replacements: [], skippedReason: `AI 脱敏失败:${error.message}` };
}
AI 失败不会阻断发布,只是跳过 AI 层,规则层的结果照常生效。
实际应用场景
场景 1:发布技术笔记
我写了一篇 VPS 配置笔记,里面有真实 IP、SSH 配置、代理密码。
发布时:
bun tools/share.ts ~/Notes/VPS配置.md
输出:
脱敏完成: 规则 8 处, AI 2 处
发布成功: https://s.chen.rs/vps-config
本地原文不变,发布内容已脱敏。
场景 2:分享排障记录
服务器被入侵的排查记录,里面有大量敏感信息(IP、指纹、时间线)。
规则层处理了 IP 和 SSH 指纹,AI 层发现了我写的 "入侵者 IP 来自
场景 3:API 使用教程
教别人用某个 API,示例代码里有真实的调用:
curl -H "Authorization: Bearer sk-proj-abc123..." https://api.example.com/v1/chat
规则自动替换 Bearer Token 和 API Key,示例代码结构保留。
我踩过的坑
1. AI 会过度脱敏
一开始 AI 会把正常的技术术语也脱敏,比如把 "使用 root 用户登录" 里的 "root" 替换成 <REDACTED_USER>。
解决方案:在 prompt 里明确 "不要改写非敏感内容",并且加了长度限制(from.length < 4 太短的不处理)。
2. AI 幻觉导致替换失败
AI 有时候会"发明"一个不存在的片段。比如原文是 password: abc123,AI 返回 {"from": "password=abc123", ...}。
解决方案:用 content.includes(from) 预检查,不存在就跳过。
3. 正则太贪婪
一开始 IP 正则会匹配版本号 <REDACTED_IP>(比如 sing-box 1.11.0)。
解决方案:加了 IPv4 有效性检查(每段 0-255)。
推荐资源
- OWASP Sensitive Data Exposure - 理解敏感信息泄露风险
- 正则表达式可视化工具 - 调试复杂正则
下一步
如果你也想在发布流程里加入脱敏:
- 从规则层开始,先覆盖你最常用的敏感信息格式
- 加入 AI 层作为兜底,但要做好校验
- 记录每次脱敏结果,定期 review 是否有漏网或误伤
记住:脱敏只作用于发布内容,本地原文不变。这样既安全,又不影响你自己使用。
附录:给 AI 的复现指令
帮我实现一个 Markdown 发布前脱敏模块,支持规则 + AI 双层检测。
目标:发布文章时自动检测并替换敏感信息(IP、API Key、密码等),本地原文不变。
技术栈:Bun + TypeScript
核心函数:
sanitizeSensitiveContent(input: string, options: { useAi?: boolean }): Promise<SanitizationResult>
返回结构:
{
content: string; // 脱敏后内容
changed: boolean; // 是否有变化
replacements: Array<{ // 替换记录
source: "rule" | "ai";
label: string;
count: number;
reason?: string;
}>;
aiSkippedReason?: string; // AI 跳过原因(如无 API Key)
}
规则层(确定性):
- IPv4 地址 → <REDACTED_IP>
- SSH 指纹 SHA256:xxx → SHA256:<REDACTED_FINGERPRINT>
- Bearer Token → <REDACTED_TOKEN>
- sk-xxx API Key → <REDACTED_API_KEY>
- hysteria2://<REDACTED_AUTH>@... → hysteria2://<REDACTED_AUTH>@...
- Cloudflare Tunnel ID → <TUNNEL_ID>
- URL ?token=xxx → ?token=<REDACTED_TOKEN>
豁免规则:
- 跳过占位符:$VAR、<PLACEHOLDER>、包含 xxx 或 ... 的
- 跳过本地 IP:127.0.0.1、0.0.0.0
AI 层(语义理解):
- 调用 OpenAI 兼容 API(走 AI_API_KEY 环境变量)
- Prompt 要求返回 JSON:{replacements: [{from, to, reason}]}
- 校验 AI 输出:from 必须在原文存在,to 必须是 <REDACTED_XXX> 格式
- 失败时静默跳过,不阻断发布
集成到发布 CLI:
- 默认启用规则 + AI
- --no-sanitize-ai 只用规则
- --no-sanitize 完全关闭
成功标志:
- 发布时输出 "脱敏完成: 规则 X 处, AI Y 处"
- 本地文件内容不变
- 发布内容敏感信息已替换