Skip to content

finding(core): ObjectLogger 的脱敏表按子串匹配,一个叫 keys 的普通字段会被整块换成 ***REDACTED*** #5573

Description

@os-zhuang

在做 #5048(PR #5572)时实测发现,顺手记录。

现象

packages/core/src/logger.tsredactSensitive 递归遍历 meta,判定条件是子串包含:

private redactSensitive(obj: any): any {
    ...
    const lower = key.toLowerCase();
    if (this.config.redact.some((p: string) => lower.includes(p.toLowerCase()))) {
        redacted[key] = '***REDACTED***';
    }

默认脱敏表(logger.ts:97,与 packages/spec/src/system/logging.zod.ts:71 的 schema 默认值一致)是 ['password', 'token', 'secret', 'key']。于是任何名字含这些子串的字段都会被整块替换,不问值是什么:

  • keys(含 key)
  • keyword / keywordskeyboardmonkey
  • tokenizertokens(计数!)
  • secretary

实测(format: 'json',值是一个普通的字符串数组):

log.warn('…', { issues: [{ code: 'unrecognized_keys', keys: ['visibleIf'], path: ['nodes', 0] }] })
→ {"…","issues":[{"code":"unrecognized_keys","keys":"***REDACTED***","path":["nodes",0]}]}

keys 里根本没有秘密,但读者拿到的是 ***REDACTED*** —— 而且这个字样会让读者以为确实有一个秘密被挡住了,比字段缺失更误导。

为什么是 finding 而不是 bug

全仓 grep 过,目前没有任何 in-tree 的 logger.* 调用在 meta 里放这类字段名(#5048 差点成为第一个 —— 那条路已经在 PR #5572 里绕开了:字段改名叫 unrecognized,并写了 pin 测试固定「原样转发会被脱敏」这个事实)。所以今天没有用户会撞上,归观察类。

ObjectLogger 是公开导出的,redact 也是 LoggerConfig 的公开配置项,host 侧插件自己写 logger.info(..., { keys: [...] }) 就会撞上,而且撞上时是静默的 —— 没有任何提示说脱敏器动过手。

取舍(不预设结论)

子串匹配不是笔误,它是为了一网打尽 apiKey / secretKey / privateKey / accessToken 这些真实拼法 —— 换成精确匹配会漏掉它们,那更危险。所以这不是「改成 equals 就好」,而是需要维护者定一个方向,几个选项:

  • A. 保持子串,只在词边界上匹配:key 匹配 apiKey/api_key(camelCase / snake_case 分词后是独立词),但不匹配 keys/monkey。能同时保住覆盖面和精度,实现是分词而不是 includes
  • B. 保持子串,加一个例外表(keyskeyword、…)。便宜,但例外表永远追不齐,是治症状。
  • C. 值敏感:只脱敏标量,不脱敏数组/对象。恰好能救 keys: [...],但 apiKeys: ['sk-…'] 就漏了 —— 方向不对。
  • D. 不改,只记入文档,让作者自己避开这些字段名。等于把一个静默数据丢失的陷阱转成文档义务。

倾向 A(词边界),因为它在「不漏真秘密」和「不吃普通字段」之间不用二选一;但这动的是脱敏语义,属于安全相关默认值,不该由一个顺手的 finding 决定 —— 留给维护者。

影响面


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions