feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
CI / 类型检查 + Lint + 单元测试 (push) Failing after 9m45s
CI / 全量测试 (Electron ABI) (push) Failing after 6m28s
CI / 产物编译验证 (push) Successful in 11m18s

This commit is contained in:
2026-09-08 14:30:27 +08:00
parent 69776e447f
commit 4cd6e997b5
86 changed files with 4303 additions and 956 deletions
+52
View File
@@ -0,0 +1,52 @@
/**
* v0.8.2 P1-5: 敏感值脱敏单源工具
*
* 背景:审计日志 logToolCall 原样落库完整工具 args —— 工具参数中携带的
* API Key / 鉴权头 / token(如 http_request 的 headers.authorization、
* MCP 工具的鉴权参数)以明文进入 audit_logs 表,密钥链加密(safeStorage
* 只保护配置层,不保护审计层,构成敏感信息二次扩散面。
*
* 本模块提供与配置层同源(isSensitiveConfigKey 的归一化键名匹配)的脱敏:
* - maskSensitiveValue:字符串值掩码(长值保留后 4 位,短值完全掩码)
* - deepMaskSensitive:递归遍历对象/数组,按键名匹配掩码字符串值
* (循环引用防护 + 深度上限,防御恶意构造的参数结构)
*/
import { isSensitiveConfigKey } from './secure-config';
/** 长值保留后 4 位,短值(≤4 字符)完全掩码 —— 与 ipc/shared.maskSensitive 同口径 */
export function maskSensitiveValue(value: string): string {
return value.length > 4 ? '***' + value.slice(-4) : '***';
}
const MAX_MASK_DEPTH = 6;
/**
* 深度脱敏:返回脱敏后的副本(原对象不修改)。
* 对象/数组递归;键名命中敏感模式(归一化匹配,api_key/authKey/token/secret/
* password/credential 等)时对字符串值掩码;其余值原样保留。
*/
export function deepMaskSensitive<T>(input: T, depth = 0, seen?: Set<object>): T {
if (input === null || typeof input !== 'object') return input;
if (depth >= MAX_MASK_DEPTH) return '[depth-limit]' as unknown as T;
const seenSet = seen ?? new Set<object>();
if (seenSet.has(input as object)) return '[circular]' as unknown as T;
seenSet.add(input as object);
try {
if (Array.isArray(input)) {
return input.map((item) => deepMaskSensitive(item, depth + 1, seenSet)) as unknown as T;
}
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (isSensitiveConfigKey(key) && typeof value === 'string' && value.length > 0) {
out[key] = maskSensitiveValue(value);
} else {
out[key] = deepMaskSensitive(value, depth + 1, seenSet);
}
}
return out as unknown as T;
} finally {
seenSet.delete(input as object);
}
}