目录
-
- 一、在哪几个地方打码
- 二、它认得哪些密钥
- [三、实测:12 条样例](#三、实测:12 条样例)
- 四、不拦输出,日志里也不留值
- 五、它遮不住的情况
- 小结
让 AI 帮忙查「为什么连不上数据库」「为什么调第三方接口返回 401」,它几乎一定会去读 .env、config.yaml,或者跑一下 env 看环境变量。
问题是,它读到的内容会作为工具结果,原样发给大模型。密钥一旦离开本机,就不归你管了。
WES Code 在这一步加了一道处理:工具结果交给模型之前,先按一张规则表给密钥打码,只遮值、保留键名,不拦输出。这篇讲它在哪几个地方打码、认得哪些密钥、我用 12 条样例实测的结果,以及它遮不住的情况。
一、在哪几个地方打码
| 位置 | 打码的是什么 |
|---|---|
| 工具结果 | 读文件、跑命令等工具的输出 |
| 模型的回复 | 推送到你面前之前 |
| 需要你确认的提示 | 弹窗里展示的命令和内容 |
工具结果这一处最关键。一个工具结果出来以后会分成三份:界面上显示一份,发回模型一份,太长的还会写到磁盘上。打码放在分叉之前做,三份拿到的都是遮过的。所以 AI 读了 .env,模型看到的就是 [REDACTED]。
比如 AI 读到这样一份 config.yaml(内容是我编的):
yaml
server:
port: 8080
database:
dsn: postgres://shop:Sh0pPassw0rd2024@localhost:5432/shop
llm:
provider: openai
api_key: sk-proj-7c1e9a4b2d8f6e3a
github:
token: ghp_9a8b7c6d5e4f3a2b1c0d
交给模型的是这样的(用第三节那套代码实际跑出来的):
yaml
server:
port: 8080
database:
dsn: postgres://shop:[REDACTED]@localhost:5432/shop
llm:
provider: openai
api_key: [REDACTED]
github:
token: [REDACTED]
端口、服务商、连接串里的用户名和主机都还在,AI 照样能看出配置哪里不对,只是拿不到那三个值。
这套密钥规则是内置的,不能通过配置关掉。
二、它认得哪些密钥
规则一共 32 条,分两类。
看格式的 11 条:
| 类型 | 长什么样 | 怎么遮 |
|---|---|---|
| 键值对 | OPENAI_API_KEY=...、api_key: ...、"token": "..." |
只遮值,键名保留 |
| 数据库连接串 | postgres://用户名:密码@主机 |
只遮密码,用户名和主机保留 |
| 请求头 | Authorization: Bearer ... |
只遮令牌 |
| 私钥 | -----BEGIN ... PRIVATE KEY----- 整段 |
换成 [REDACTED PRIVATE KEY] |
| JWT | eyJ 开头的三段式令牌 |
换成 [REDACTED JWT] |
| 云服务密钥 | AWS 的 AKIA...、Azure 订阅密钥等 |
只遮值 |
键值对认的是键名里的关键词:api_key、apikey、token、secret、password、passwd、credential、access_key。前后带别的字符也算,所以 MY_OPENAI_API_KEY、providerApiKey 都认得。等号、冒号都认,.env、YAML、JSON 三种写法都覆盖。
看前缀的 21 条: 很多服务商的密钥有固定开头,比如 OpenAI 的 sk-、GitHub 的 ghp_、Hugging Face 的 hf_、npm 的 npm_、阿里云的 LTAI。这类不需要键名,光看开头就能认出来。
三、实测:12 条样例
我把这套规则的代码原样拿出来,写了个小程序,对 12 条样例各跑一遍。样例里的密钥都是我编的。
go
package main
import (
"fmt"
"credscandemo/credscan" // WES Code 的密钥规则,原样复制
)
func main() {
samples := []string{
`OPENAI_API_KEY=sk-proj-4f8a1c9e2b7d6a3f`,
`api_key: sk-9d2c7b4e1a8f3c6d`,
`{"token": "a1b2c3d4e5f6g7h8"}`,
`DATABASE_URL=postgres://app:S3cretPassw0rd@localhost:5432/shop`,
`Authorization: Bearer abcdef1234567890`,
`eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NSJ9.c2lnbmF0dXJlMTIz`,
"-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA7x9k\n-----END RSA PRIVATE KEY-----",
`OPENAI_API_KEY=your-api-key-here`,
`aws_access_key_id = AKIAIOSFODNN7EXAMPLE`,
`ghp_abc123def456ghi789`,
`换成 sk-short 试试`,
`password: abc123`,
}
for _, s := range samples {
fmt.Println(credscan.Redact(s))
}
}
结果:
| # | 输入 | 输出 |
|---|---|---|
| 1 | OPENAI_API_KEY=sk-proj-4f8a1c9e2b7d6a3f |
OPENAI_API_KEY=[REDACTED] |
| 2 | api_key: sk-9d2c7b4e1a8f3c6d |
api_key: [REDACTED] |
| 3 | {"token": "a1b2c3d4e5f6g7h8"} |
{"token": "[REDACTED]"} |
| 4 | DATABASE_URL=postgres://app:S3cretPassw0rd@localhost:5432/shop |
DATABASE_URL=postgres://app:[REDACTED]@localhost:5432/shop |
| 5 | Authorization: Bearer abcdef1234567890 |
Authorization: Bearer [REDACTED] |
| 6 | 一段 JWT | [REDACTED JWT] |
| 7 | 一段 RSA 私钥 | [REDACTED PRIVATE KEY] |
| 8 | OPENAI_API_KEY=your-api-key-here |
原样不动 |
| 9 | aws_access_key_id = AKIAIOSFODNN7EXAMPLE |
aws_access_key_id = AKIA[REDACTED] |
| 10 | ghp_abc123def456ghi789 |
ghp_[REDACTED] |
| 11 | 换成 sk-short 试试 |
原样不动 |
| 12 | password: abc123 |
原样不动 |
有几条值得单独说:
- 第 1 条 :值本身也是
sk-开头,但键值对规则先执行,先把值遮掉了,所以输出里OPENAI_API_KEY=还在,读起来仍然是一行配置。 - 第 4 条 :只遮了密码,用户名
app、主机和库名都在。查「连不上数据库」时,这些信息照样有用。 - 第 8 条 :文档里常见的占位写法,不打码。键值对规则会跳过含
your、example、placeholder、<、${这类词的值,不然每个.env.example都会被遮得面目全非。 - 第 9 条 :这是 AWS 官方文档里的示例密钥,结尾是
EXAMPLE。键值对规则把它当成占位符跳过了,但「AKIA开头」这条规则不看占位词,照样遮。按前缀认的规则都是这样:格式对上了就遮,不因为里面有个单词就放过。
另外,同一段文字打两次码,结果不变:已经是 [REDACTED] 的地方不会被再遮一遍。
第 11、12 条为什么没遮,放到第五节说。
四、不拦输出,日志里也不留值
打码只替换值,不会因为发现了密钥就把整个结果拦下来。拦下来的话,AI 读 .env 查问题这件事就做不成了;遮掉值,结果里其余的配置照样能用。
发生打码时,日志里记一条警告,内容是命中了几条、第一条是哪条规则,不记值本身。命中的描述长这样(第 1 条样例的扫描结果):
secret_assignment env_secret REDACTED 24 chars
openai_key vendor_key REDACTED 21 chars
只留规则名和长度。长度能帮你区分这是一个 8 位的密码还是一串 64 位的令牌,又不会泄露内容。
五、它遮不住的情况
- 太短的值 :键值对的值少于 8 个字符不遮,比如第 12 条的
password: abc123;按前缀认的,前缀后面少于 10 个字符不遮,比如第 11 条的sk-short。这是为了不把正文里随手提到的sk-short当成密钥 - 键名不在关键词里 :
DB_PASS=Hunter2Hunter2我也试了,没遮。pass不在关键词里,只有passwd、password算 - 值里碰巧带了占位词 :
api_key: my_real_key_9f8e7d没遮,因为值里有my_。同样的值去掉my_就遮了 - 自定义格式:公司内部自己定的令牌,键名不带关键词、开头也不是已知前缀,认不出来
- 报错结果:工具返回的是错误结果时,不走工具结果这一步打码
- 只管发出去的内容 :打码改的是发给模型、显示在界面上的文字,磁盘上的
.env不会被改
「键名不在关键词里」和「值里带占位词」这两条,是用下面这个测试核对的:
go
func TestNotCovered(t *testing.T) {
for _, s := range []string{
`DB_PASS=Hunter2Hunter2`,
`api_key: my_real_key_9f8e7d`,
`api_key: real_key_9f8e7d`,
} {
t.Logf("%s => %s", s, credscan.Redact(s))
}
}
前两条原样输出;第三条是同一个值去掉 my_,输出变成了 api_key: [REDACTED]。
小结
- 工具结果在交给模型之前先打码,模型的回复、需要你确认的提示也会打码;内置规则关不掉
- 32 条规则:11 条看格式,21 条看服务商前缀;只遮值,键名、用户名、主机都保留
- 文档占位符不遮,AWS 文档示例这种真格式照样遮;不拦输出,日志里只留规则名和长度
- 太短的值、不认识的键名和格式、值里带占位词的、报错结果,遮不住
文中的密钥打码是 WES Code 里的功能,官网是 weisyn.com。你们项目里的密钥一般放在哪,让 AI 读配置时担心过吗,欢迎评论区聊聊。
觉得有用的朋友,欢迎点赞、收藏、关注,后面会继续分享 AI 编程的实战经验。