脱敏与拦截
凭据明文出了金库,下一道闸门是 Hook。本篇讲它在做什么、它的边界。
两个拦截点
AI 客户端 Sigil
│ │
│ tools/call: http_request(GET api.github.com/...) │
├──────────────────────────────────────────────────────►│
│ │
│ ┌───────────────────────────────┴───┐
│ │ PreToolUse Hook │
│ │ - 解密 github-personal │
│ │ - 注入 Authorization: token xxx │
│ │ - 记录"将要执行" │
│ └─────────────┬─────────────────────┘
│ │
│ ▼
│ (真实 HTTP 请求送到 api.github.com)
│ │
│ ▼
│ (响应回来:可能含 ghp_xxx 残影)
│ │
│ ┌─────────────┴─────────────────────┐
│ │ PostToolUse Hook │
│ │ - 正则扫描响应 │
│ │ - 匹配凭据模式 → 替换 [REDACTED] │
│ │ - 记录"已执行完成" │
│ └─────────────┬─────────────────────┘
│ │
│ result: { ..., "token": "[REDACTED:github_token]", ... } │
│◄──────────────────────────────────────────────────────── │两道闸门都在 Sigil 进程内——AI 客户端看不见它们存在。
PreToolUse:注入
调用方说"我要发 HTTP 请求"——Sigil 在请求真的出去之前:
- 解析目标 URL(host + path)
- 根据 host 匹配凭据:
api.github.com→ 找 type=github_token的凭据gitee.com/api→ 找gitee_token- 内部 API host → 用模板里指定的
http_api凭据
- 校验 Scope Policy
- 从 OS 密钥环解出明文(短暂在内存,zeroize 包装)
- 注入到请求头/参数
- 真的发出请求
- 明文离开作用域自动清零
明文存活时间 < 1 ms。客户端从来没看到。
PostToolUse:脱敏
请求成功回来后,响应里有可能含凭据残影——比如:
- GitHub API 把 Token 的最后 4 位 echo 回来
- 某个 webhook 配置返回
secret: xxx - 错误信息里包含完整 URL(含 query string 中的 key)
Sigil 不假设这些情况都不会发生。PostToolUse 用三层过滤:
第一层:已知凭据模式
对当前金库中所有凭据明文做一次模式扫描——如果响应里出现了任何凭据的明文,立即替换为 [REDACTED:credential_<id>]。
这是最严格的一层:是基于事实(你金库里真有的明文)而非启发式("看起来像 Token")。
第二层:通用凭据模式
常见格式的正则匹配:
| 模式 | 替换为 |
|---|---|
ghp_[A-Za-z0-9]{36} | [REDACTED:github_token] |
github_pat_[A-Za-z0-9_]{82} | [REDACTED:github_fine_grained] |
gho_[A-Za-z0-9]{36} | [REDACTED:github_oauth] |
gitee_[A-Za-z0-9]{32} | [REDACTED:gitee_token] |
sk-[A-Za-z0-9]{48} | [REDACTED:openai_key] |
sk-ant-[A-Za-z0-9-_]{95,} | [REDACTED:anthropic_key] |
xoxb-[A-Za-z0-9-]+ | [REDACTED:slack_bot_token] |
MySQL/Postgres 连接串里的 :password@ | [REDACTED:db_password] |
AWS Access Key (AKIA...) | [REDACTED:aws_key] |
完整规则列表内置在 Sigil,随版本更新。
第三层:通用秘密启发式
最后兜底,检测:
- 长度 ≥ 20 的高熵字符串
- 紧跟
password=/token=/secret=/key=的值 - Bearer 鉴权头
这一层会有误报(普通的长 UUID 也可能被打掉)。Sigil 用统计阈值控制:误报率高时会降级为只标注不替换,让用户决定。
脱敏的诚实
三层脱敏不构成 100% 拦截承诺。
正则与启发式有天然边界:
- 自定义格式的内部 Token(公司自创 schema)
- Base64 编码后嵌入更长字符串的凭据
- 多次拼接 / 加密后的"派生形态"
- 通过侧信道(响应长度 / 时间)泄露的元信息
Sigil 的脱敏是降低风险而非消除风险。配合 审计日志 的事后追溯 + Scope Policy 的事前限制,整体安全水平才是合格的。
客户端层面的接力
脱敏只在Sigil 出口生效。结果到了 AI 客户端之后:
- 客户端可能把结果存进会话历史(用户可见)
- 客户端可能上传给 AI 厂商做推理(厂商策略决定)
- 客户端可能在 UI 里展示完整 JSON(截图风险)
这些是 Sigil 范围之外的——但合规做法是:
- 主流 AI 厂商(Anthropic / OpenAI)都有 API 数据使用策略
- 客户端可以配置"工具结果不进训练数据"
- 用户可以在 Sigil 设置里限制"哪些能力允许返回大段文本"
误报与豁免
误报场景例:
- 你查
git log,commit hash 是 SHA-1(40 字符 hex)—— 被启发式打掉 - 你查 JIRA issue,ticket key 紧跟
key=PROJ-123—— 被启发式打掉
Sigil 给出两个豁免机制:
- 能力级豁免:在能力配置里标"不做启发式脱敏"(如
git_query)—— 这种能力的返回值通常是结构化的,已知不含凭据 - 域名级豁免:标"这个域名的响应不脱敏"(如自家 status page)
豁免只对启发式(第三层)生效——前两层(已知明文匹配、已知模式匹配)任何时候都不豁免。
Hook 永远不可旁路
Hook 是 Sigil 内置的,不是可插拔的脚本。理由:
- 可插拔脚本意味着可以禁用
- 第三方脚本意味着审计无法覆盖
- 这是安全防线,不是用户偏好
如果未来需要"用户自定义脱敏规则",会以追加规则的形式(不能删除内置)实现。
测试自己的脱敏
Sigil 内置一个"脱敏自检"工具(设置 → 安全 → 脱敏自检):
- 把任意一段文本贴进来
- 模拟运行三层 Hook
- 看输出(红色高亮被打掉的部分)
用来快速验证"我担心的某种泄漏路径,Sigil 是否能拦住"。
