P1 修复面收口: - 超时三态区分(aborted→USER_INTERRUPT / ETIMEDOUT→TIMEOUT / 其余→ERROR), 根治"真实网络超时被误报为用户中断" - 流空闲超时统一(SSE/Ollama/Anthropic 读循环 60s 无数据抛 504 进重试通道) - 同会话并发 sendMessage 防重入(isRunning 守卫)+ 会话存在性预检 + 前置调用移入 try(ERROR+DONE 双事件保证,根治 isStreaming 假死) - 清空审计后 resetChainCache(根治 verifyChain 误报 TAMPERED) - DONE 不再提前清理 TRACE(TERMINATED 统一收尾,补全最终迭代录制) - IME 合成回车不发送(普通 Enter + Cmd/Ctrl+Enter 双分支)+ handleSend 闭包修复 P2 安全纵深: - preload 移除原始 electronAPI 暴露(渲染层零使用,关掉 XSS invoke 任意通道单点风险) - CORS 同源回显根治(仅当前浏览页面 Origin,did-navigate 同步) - MEMORY.md 命令保护正则扩展(括号/$/反引号/< 重定向边界 + 前导路径) - write_file append TOCTOU 统一(open 后 realpath 校验,新文件分支补漏) - 敏感键归一化(authKey 驼峰/连字符命中)+ MCP headers 鉴权值加密落库 - ReDoS 检测共享化(search_files/file_editor 统一拦截) - run_tests/lint_code 升风险 + 需确认 + npx --no-install(执行边界对齐 run_command) - MCP/SearXNG/llm.baseURL/updateFeedUrl 配置类 URL 高危目标校验(IPv6 去括号 + 十六进制映射解析 + 尾点剥离) P3 架构还债: - temperature/maxTokens 热生效(引擎/编排器/SubAgent 三处接线)+ setBatch 单事务落盘 - SessionRecorder flush 竞态根治(flushPromise 等待 + 超限内联落盘 + stopRecording async) - 内存收口(lastConsolidationBySession LRU / subTraces 清理 / 会话删除 disposeEngine) - i18n 全量收口(28 组件 + 353 key 双字典,状态标签改渲染时函数) - 死代码清理(updateTraceStep/HEADER_HEIGHT/void preA/失实注释) - 斜杠菜单 MUI 化 + 删除逻辑收敛 resetSessionState + Blob URL 统一释放 + 用户消息"仅保存"落库(saveMessage 透传前端 id 修复 id 错位) P4 能力演进: - 死循环检测拆分(驻留前置 + 乒乓后置带进度信号,合法交替不误报) - run-lock 30s 超时强制 abort(旧 run 卡死不无限排队) - RETRY 双通道 stream_reset(前端按 run 归属精确清空,根治重试文本重复) - FTS5 trigram 中文子串搜索(迁移 9 版本化 SCHEMA_VERSION=2,≤2 字符 LIKE 回退) - getContextWindow 兜底 1M→128K(未知模型防 413) 测试: - 855 → 2406 用例(+1551,2.8 倍):服务层 +325(含 MemoryManager 51 新用例)、 工具实体 +483、IPC/适配器 +390(含 OpenAI/Anthropic/Ollama 独立套件)、 纯函数表格化 +330;引入 jsdom + @testing-library(14 组件测试文件 249 用例) - 修复 R1(saveMessage id 透传)/ R2(stream_reset 精确归属)两个回归缺陷 - 遗留低危项清零:git-tools 顺序耦合 / web-fetch 真实时间退避 / slo 内存断言 / mcp-security 多余 skipIf / deepseek-balance 命名误导 / 组件 mock 注入脆弱性 版本: 0.7.4; README 同步(工具风险表/版本徽章); 依赖: 移除 @electron-toolkit/preload, 新增 jsdom/@testing-library(devDependencies 不打包) 回归: typecheck 双端 0 错误; ESLint 0/0; Electron ABI 全量 2406/2406 零跳过; 系统 Node 2110 通过 296 跳过(better-sqlite3 ABI)
475 lines
17 KiB
TypeScript
475 lines
17 KiB
TypeScript
/**
|
||
* Policy Engine — 权限策略引擎
|
||
*
|
||
* 三级权限模型:Read / Write / External Action
|
||
*
|
||
* v0.3.0 增强:
|
||
* - 实现 maxFrequency 频率限制(滑动窗口算法)
|
||
* - 添加 checkFrequency 和 recordCall 方法
|
||
* - 添加 cleanupFrequencyRecords 防止内存泄漏
|
||
*
|
||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第十章
|
||
*/
|
||
|
||
export enum PermissionLevel {
|
||
READ = 'read',
|
||
WRITE = 'write',
|
||
EXTERNAL_ACTION = 'external',
|
||
}
|
||
|
||
export interface PermissionPolicy {
|
||
toolName: string;
|
||
requiredLevel: PermissionLevel;
|
||
allowedPatterns?: RegExp[];
|
||
deniedPatterns?: RegExp[];
|
||
maxFrequency?: number;
|
||
requireConfirmation?: boolean;
|
||
}
|
||
|
||
export const DEFAULT_POLICIES: PermissionPolicy[] = [
|
||
// v0.3.0 修复:deniedPatterns 使用 (?:\/|["'\s,}]|$) 匹配,
|
||
// 覆盖 /etc/ 和 /etc(无尾斜杠,在 JSON 字符串中后跟引号的情况)
|
||
// H-5 修复: 移除 /MEMORY\.md/i 粗粒度正则 — 之前会误拦子目录的 MEMORY.md
|
||
// 改为在 engine.ts executeToolSafely 中进行精确的根目录校验(仅保护 workspacePath/MEMORY.md)
|
||
// @see project_memory.md — Only the MEMORY.md in the workspace root directory is protected
|
||
{
|
||
toolName: 'read_file',
|
||
requiredLevel: PermissionLevel.READ,
|
||
deniedPatterns: [
|
||
/\/etc(?:\/|["'\s,}]|$)/,
|
||
/\/proc(?:\/|["'\s,}]|$)/,
|
||
/C:\\Windows\\/i,
|
||
/C:\\System32\\/i,
|
||
],
|
||
},
|
||
{ toolName: 'web_search', requiredLevel: PermissionLevel.READ, maxFrequency: 10 },
|
||
{ toolName: 'list_directory', requiredLevel: PermissionLevel.READ },
|
||
{ toolName: 'search_files', requiredLevel: PermissionLevel.READ },
|
||
{ toolName: 'memory_search', requiredLevel: PermissionLevel.READ },
|
||
{
|
||
toolName: 'write_file',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
deniedPatterns: [
|
||
/\/etc(?:\/|["'\s,}]|$)/,
|
||
/\/proc(?:\/|["'\s,}]|$)/,
|
||
/\/System(?:\/|["'\s,}]|$)/,
|
||
/C:\\Windows\\/i,
|
||
/C:\\System32\\/i,
|
||
],
|
||
requireConfirmation: true,
|
||
maxFrequency: 5,
|
||
},
|
||
{ toolName: 'memory_store', requiredLevel: PermissionLevel.WRITE },
|
||
{
|
||
toolName: 'run_command',
|
||
requiredLevel: PermissionLevel.EXTERNAL_ACTION,
|
||
deniedPatterns: [/MEMORY\.md/i],
|
||
requireConfirmation: true,
|
||
maxFrequency: 10,
|
||
},
|
||
{ toolName: 'web_fetch', requiredLevel: PermissionLevel.READ },
|
||
// web_browser — 统一浏览器工具(合并自 9 个独立 browser_* 工具)
|
||
// 由于该工具可执行 JS、点击元素等高风险操作,统一设为 EXTERNAL_ACTION
|
||
{
|
||
toolName: 'web_browser',
|
||
requiredLevel: PermissionLevel.EXTERNAL_ACTION,
|
||
requireConfirmation: true,
|
||
maxFrequency: 20,
|
||
},
|
||
// v0.3.0 修复: 补全缺失的工具策略 — 之前这5个工具未配置策略,导致被 PolicyEngine 拦截
|
||
// file_editor — 精准文件编辑(WRITE),与 write_file 同级安全约束
|
||
{
|
||
toolName: 'file_editor',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
deniedPatterns: [
|
||
/\/etc(?:\/|["'\s,}]|$)/,
|
||
/\/proc(?:\/|["'\s,}]|$)/,
|
||
/\/System(?:\/|["'\s,}]|$)/,
|
||
/C:\\Windows\\/i,
|
||
/C:\\System32\\/i,
|
||
],
|
||
requireConfirmation: true,
|
||
maxFrequency: 10,
|
||
},
|
||
// code_search — 基于 ripgrep 的只读搜索(READ)
|
||
{ toolName: 'code_search', requiredLevel: PermissionLevel.READ },
|
||
// diff_viewer — 文件/文本差异对比(只读,READ)
|
||
{ toolName: 'diff_viewer', requiredLevel: PermissionLevel.READ },
|
||
// task_manager — 任务管理(数据库读写,低风险 WRITE)
|
||
{ toolName: 'task_manager', requiredLevel: PermissionLevel.WRITE },
|
||
// delegate_task — 子任务委派(启动 SubAgent,EXTERNAL_ACTION)
|
||
{
|
||
toolName: 'delegate_task',
|
||
requiredLevel: PermissionLevel.EXTERNAL_ACTION,
|
||
requireConfirmation: false,
|
||
maxFrequency: 5,
|
||
},
|
||
// C-7 修复: MCP 工具通配符策略 — MCP 工具名称动态生成(mcp_{serverName}_{toolName})
|
||
// 无法预先配置精确策略,使用 mcp_* 通配符匹配所有 MCP 工具
|
||
// @see project_memory.md — All tools must have a configured policy in DEFAULT_POLICIES
|
||
{
|
||
toolName: 'mcp_*',
|
||
requiredLevel: PermissionLevel.EXTERNAL_ACTION,
|
||
requireConfirmation: true,
|
||
maxFrequency: 20,
|
||
},
|
||
|
||
// v0.3.1: Git 工具集(4 个)
|
||
{ toolName: 'git_status', requiredLevel: PermissionLevel.READ },
|
||
{ toolName: 'git_diff', requiredLevel: PermissionLevel.READ },
|
||
{ toolName: 'git_log', requiredLevel: PermissionLevel.READ },
|
||
{
|
||
toolName: 'git_commit',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
requireConfirmation: true,
|
||
maxFrequency: 10,
|
||
},
|
||
|
||
// v0.3.1: 开发工具集(3 个)
|
||
// v0.7.4 P2-8: lint_code 升 WRITE + 确认 —— 其通过 npx tsc/eslint 执行工作区代码,
|
||
// 与 run_command 的执行边界对齐;run_tests 升 WRITE + 确认 —— npm test 执行
|
||
// package.json scripts.test 的任意命令(被污染工作区可诱导任意代码执行)。
|
||
{
|
||
toolName: 'lint_code',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
requireConfirmation: true,
|
||
maxFrequency: 20,
|
||
},
|
||
{
|
||
toolName: 'run_tests',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
requireConfirmation: true,
|
||
maxFrequency: 10,
|
||
},
|
||
{ toolName: 'project_info', requiredLevel: PermissionLevel.READ },
|
||
|
||
// v0.3.1: HTTP 请求工具(1 个)
|
||
{ toolName: 'http_request', requiredLevel: PermissionLevel.READ, maxFrequency: 20 },
|
||
|
||
// v0.3.1: 结构化思考工具(1 个)— 无副作用
|
||
{ toolName: 'think', requiredLevel: PermissionLevel.READ },
|
||
|
||
// v0.3.1: 图片查看工具(1 个)— 只读
|
||
{ toolName: 'view_image', requiredLevel: PermissionLevel.READ },
|
||
|
||
// v0.3.2: 文件删除工具(1 个)— 破坏性操作,必须确认
|
||
{
|
||
toolName: 'delete_file',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
requireConfirmation: true,
|
||
maxFrequency: 30,
|
||
},
|
||
|
||
// v0.3.3: 文件移动/重命名工具(1 个)— 可能覆盖目标,需确认
|
||
{
|
||
toolName: 'file_move',
|
||
requiredLevel: PermissionLevel.WRITE,
|
||
requireConfirmation: true,
|
||
maxFrequency: 30,
|
||
},
|
||
|
||
// v0.3.3: 文件信息查询工具(1 个)— 只读
|
||
{ toolName: 'file_info', requiredLevel: PermissionLevel.READ },
|
||
];
|
||
|
||
export class PolicyEngine {
|
||
private policies: Map<string, PermissionPolicy> = new Map();
|
||
|
||
/**
|
||
* v0.4.1: 工具调用频率追踪 — 频率 key -> 调用时间戳列表
|
||
* key 格式: `${sessionId}:${toolName}`(会话隔离)
|
||
* 历史问题:v0.3.0 以 toolName 为 key,所有会话共享同一配额——
|
||
* P2-10 支持多会话并发后,一个会话可耗尽另一个会话的配额(如 web_search 10 次/分钟)
|
||
*/
|
||
private callFrequency: Map<string, number[]> = new Map();
|
||
|
||
/** v0.3.0: 频率限制的时间窗口(1分钟 = 60秒) */
|
||
private readonly FREQ_WINDOW_MS = 60_000;
|
||
|
||
/** v0.4.1: 构造会话隔离的频率 key(sessionId 缺失时回退 'global' 保持兼容) */
|
||
private freqKey(toolName: string, sessionId?: string): string {
|
||
return `${sessionId || 'global'}:${toolName}`;
|
||
}
|
||
|
||
/**
|
||
* v0.3.0 修复:customPolicies 与 DEFAULT_POLICIES 合并而非完全覆盖
|
||
*
|
||
* 合并策略:customPolicies 中的字段覆盖默认策略的同名字段,
|
||
* 未指定的字段保留默认值(如 deniedPatterns 等安全配置不会被丢失)
|
||
*/
|
||
constructor(customPolicies: PermissionPolicy[] = []) {
|
||
for (const policy of DEFAULT_POLICIES) {
|
||
this.policies.set(policy.toolName, { ...policy });
|
||
}
|
||
for (const policy of customPolicies) {
|
||
const existing = this.policies.get(policy.toolName);
|
||
if (existing) {
|
||
// v0.3.0 修复:合并而非替换,保留默认的安全配置(如 deniedPatterns)
|
||
this.policies.set(policy.toolName, { ...existing, ...policy });
|
||
} else {
|
||
this.policies.set(policy.toolName, policy);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* v0.6.4: 解析工具对应的策略(精确名 → 通配符前缀 → 无)
|
||
* 供 checkAuthorization 与 requiresConfirmation 共用匹配逻辑,消除双份漂移。
|
||
*/
|
||
private resolvePolicy(toolName: string): PermissionPolicy | undefined {
|
||
const exact = this.policies.get(toolName);
|
||
if (exact) return exact;
|
||
// C-7 修复: 支持通配符策略匹配(如 mcp_* 匹配所有 MCP 工具)
|
||
// MCP 工具名称动态生成(mcp_{serverName}_{toolName}),无法预先配置精确策略
|
||
for (const [pattern, p] of this.policies) {
|
||
if (pattern.endsWith('*') && toolName.startsWith(pattern.slice(0, -1))) {
|
||
return p;
|
||
}
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
/**
|
||
* v0.6.4 P2-1: 查询某工具按策略引擎的配置是否需要用户确认。
|
||
*
|
||
* 背景(跨层防线不一致):ConfirmationHook 原先只读工具定义的
|
||
* `requiresPermission || riskLevel∈{high,critical}` —— 而 MCPToolAdapter 把
|
||
* 全部 MCP 工具标为 requiresPermission:false + MEDIUM,导致 PolicyEngine 为
|
||
* `mcp_*` 配置的 requireConfirmation 形同虚设:外部 MCP server 的任意工具
|
||
* 都被免确认执行。此方法让 ConfirmationHook 能消费策略层的声明。
|
||
*
|
||
* @param toolName 工具名
|
||
* @returns true 表示有策略且其 requireConfirmation=true;无策略时返回 false
|
||
* (未知工具由 PermissionCheckHook 的 fail-closed 负责拒绝)
|
||
*/
|
||
requiresConfirmation(toolName: string): boolean {
|
||
return this.resolvePolicy(toolName)?.requireConfirmation ?? false;
|
||
}
|
||
|
||
/**
|
||
* 权限校验
|
||
*
|
||
* v0.4.1: 新增可选 sessionId 参数 — 频率限制按会话隔离(多会话并发时各自独立配额)
|
||
*
|
||
* @param toolName 工具名
|
||
* @param args 工具参数
|
||
* @param sessionId 会话 ID(可选;缺失时频率配额计入 'global' 桶保持向后兼容)
|
||
*/
|
||
checkAuthorization(
|
||
toolName: string,
|
||
args: Record<string, unknown>,
|
||
sessionId?: string,
|
||
): {
|
||
authorized: boolean;
|
||
reason?: string;
|
||
level: PermissionLevel;
|
||
requiresConfirmation: boolean;
|
||
} {
|
||
// v0.6.4: 复用统一的策略解析(精确 → 通配符 → 无)
|
||
const policy = this.resolvePolicy(toolName);
|
||
|
||
if (!policy) {
|
||
return {
|
||
authorized: false,
|
||
reason: `No policy configured for tool: ${toolName}`,
|
||
level: PermissionLevel.EXTERNAL_ACTION,
|
||
requiresConfirmation: true,
|
||
};
|
||
}
|
||
|
||
// v0.3.0 修复:使用 try-catch 防止循环引用导致 JSON.stringify 抛错
|
||
let argsStr: string;
|
||
try {
|
||
argsStr = JSON.stringify(args);
|
||
} catch {
|
||
// 循环引用等异常情况,降级为 toString
|
||
argsStr = String(args);
|
||
}
|
||
|
||
// #14 修复: 深度递归扫描所有字符串字段值,防止 Unicode 转义/嵌套对象/字符串拼接绕过
|
||
// 原 JSON.stringify 后整体正则匹配可被 \u0029 编码、嵌套对象伪装、正则注入等方式绕过
|
||
// 现对每个字符串字段值单独匹配,同时保留整体 argsStr 兜底匹配
|
||
const stringValues = this.deepScanStrings(args);
|
||
|
||
// v0.2.0: allowedPatterns 白名单校验 — 若定义了白名单,参数必须匹配其中之一
|
||
// #14: 字段级匹配 + 整体兜底,任一匹配即通过
|
||
if (policy.allowedPatterns && policy.allowedPatterns.length > 0) {
|
||
let matchedAllowed = false;
|
||
// 先做字段级匹配
|
||
for (const val of stringValues) {
|
||
for (const pattern of policy.allowedPatterns) {
|
||
if (pattern.test(val)) {
|
||
matchedAllowed = true;
|
||
break;
|
||
}
|
||
}
|
||
if (matchedAllowed) break;
|
||
}
|
||
// 兜底:整体 argsStr 匹配
|
||
if (!matchedAllowed) {
|
||
for (const pattern of policy.allowedPatterns) {
|
||
if (pattern.test(argsStr)) {
|
||
matchedAllowed = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!matchedAllowed) {
|
||
return {
|
||
authorized: false,
|
||
reason: 'Arguments do not match any allowed pattern',
|
||
level: policy.requiredLevel,
|
||
requiresConfirmation: policy.requireConfirmation ?? false,
|
||
};
|
||
}
|
||
}
|
||
|
||
// #14 修复: deniedPatterns 字段级深度扫描 — 对每个字符串值单独匹配
|
||
// 防止危险内容隐藏在嵌套对象或 Unicode 编码中绕过整体 stringify 匹配
|
||
if (policy.deniedPatterns) {
|
||
for (const val of stringValues) {
|
||
for (const pattern of policy.deniedPatterns) {
|
||
if (pattern.test(val)) {
|
||
return {
|
||
authorized: false,
|
||
reason: 'Command blocked by security policy',
|
||
level: policy.requiredLevel,
|
||
requiresConfirmation: false,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
// 兜底:整体 argsStr 匹配(保留原有行为,捕获跨字段拼接的危险模式)
|
||
for (const pattern of policy.deniedPatterns) {
|
||
if (pattern.test(argsStr)) {
|
||
return {
|
||
authorized: false,
|
||
reason: 'Command blocked by security policy',
|
||
level: policy.requiredLevel,
|
||
requiresConfirmation: false,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
// v0.3.0: 频率限制检查(v0.4.1: 按会话隔离)
|
||
if (policy.maxFrequency !== undefined) {
|
||
const freqCheck = this.checkFrequency(toolName, policy.maxFrequency, sessionId);
|
||
if (!freqCheck.allowed) {
|
||
return {
|
||
authorized: false,
|
||
reason: freqCheck.reason,
|
||
level: policy.requiredLevel,
|
||
requiresConfirmation: false,
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
authorized: true,
|
||
level: policy.requiredLevel,
|
||
requiresConfirmation: policy.requireConfirmation ?? false,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* v0.3.0: 频率限制检查(滑动窗口算法)
|
||
*
|
||
* 检查指定工具在时间窗口内的调用次数是否超过限制。
|
||
* 注意:此方法仅检查,不记录调用。调用成功后需调用 recordCall()。
|
||
*
|
||
* v0.3.0 修复:
|
||
* - 将 validCalls 写回 Map,避免 callFrequency 数组无限增长(内存泄漏)
|
||
*
|
||
* v0.4.1: 新增可选 sessionId 参数 — 频率配额按会话隔离
|
||
*
|
||
* @param toolName 工具名称
|
||
* @param maxFreq 最大频率(每分钟)
|
||
* @param sessionId 会话 ID(可选;缺失时计入 'global' 桶)
|
||
* @returns 检查结果
|
||
*/
|
||
checkFrequency(
|
||
toolName: string,
|
||
maxFreq?: number,
|
||
sessionId?: string,
|
||
): { allowed: boolean; reason?: string } {
|
||
const policy = this.policies.get(toolName);
|
||
const limit = maxFreq ?? policy?.maxFrequency;
|
||
if (limit === undefined) return { allowed: true };
|
||
|
||
const key = this.freqKey(toolName, sessionId);
|
||
const now = Date.now();
|
||
const calls = this.callFrequency.get(key) ?? [];
|
||
// 移除时间窗口外的调用记录
|
||
const validCalls = calls.filter((t) => now - t < this.FREQ_WINDOW_MS);
|
||
|
||
// v0.3.0 修复:将清理后的 validCalls 写回 Map,避免数组无限增长
|
||
if (validCalls.length !== calls.length) {
|
||
this.callFrequency.set(key, validCalls);
|
||
}
|
||
|
||
if (validCalls.length >= limit) {
|
||
return {
|
||
allowed: false,
|
||
reason: `Rate limit exceeded for ${toolName}: max ${limit} calls per minute (current: ${validCalls.length})`,
|
||
};
|
||
}
|
||
return { allowed: true };
|
||
}
|
||
|
||
/**
|
||
* v0.3.0: 记录工具调用(工具成功执行后调用)
|
||
*
|
||
* v0.3.0 修复:同时清理过期记录,防止数组无限增长
|
||
* v0.4.1: 新增可选 sessionId 参数 — 与 checkFrequency 的会话隔离配对使用
|
||
*
|
||
* @param toolName 工具名称
|
||
* @param sessionId 会话 ID(可选;缺失时计入 'global' 桶)
|
||
*/
|
||
recordCall(toolName: string, sessionId?: string): void {
|
||
const key = this.freqKey(toolName, sessionId);
|
||
const now = Date.now();
|
||
const calls = this.callFrequency.get(key) ?? [];
|
||
// v0.3.0 修复:记录新调用时同时清理过期记录
|
||
const validCalls = calls.filter((t) => now - t < this.FREQ_WINDOW_MS);
|
||
validCalls.push(now);
|
||
this.callFrequency.set(key, validCalls);
|
||
}
|
||
|
||
// v0.3.0 修复: cleanupFrequencyRecords 已删除 — checkFrequency 和 recordCall 已做内联清理,
|
||
// 该方法属于死代码,删除以减少维护负担
|
||
|
||
/**
|
||
* #14 修复: 深度递归扫描对象,提取所有字符串字段值
|
||
*
|
||
* 替代 JSON.stringify 后整体正则匹配的方式,防止:
|
||
* - Unicode 转义绕过(\u0029 等编码)
|
||
* - 嵌套对象伪装({a: {b: 'dangerous'}})
|
||
* - 字符串拼接绕过('rm ' + '-rf /' 在 stringify 后是合法字符串)
|
||
* - 正则注入(输入中包含正则元字符破坏匹配逻辑)
|
||
*
|
||
* @param obj 待扫描的对象
|
||
* @returns 所有字符串字段值的数组
|
||
*/
|
||
// 审查修复: 添加 visited Set 参数防止循环引用导致无限递归栈溢出
|
||
private deepScanStrings(obj: unknown, visited: Set<unknown> = new Set()): string[] {
|
||
const results: string[] = [];
|
||
if (typeof obj === 'string') {
|
||
results.push(obj);
|
||
} else if (Array.isArray(obj)) {
|
||
if (visited.has(obj)) return results;
|
||
visited.add(obj);
|
||
for (const item of obj) {
|
||
results.push(...this.deepScanStrings(item, visited));
|
||
}
|
||
} else if (obj && typeof obj === 'object') {
|
||
if (visited.has(obj)) return results;
|
||
visited.add(obj);
|
||
for (const v of Object.values(obj)) {
|
||
results.push(...this.deepScanStrings(v, visited));
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
}
|