feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强

本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题,
并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。

Critical (10/10 完成):
- C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配
  可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过

High (11/11 完成):
- 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等

Medium (55/55 完成):
- 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、
  React 组件 cancelled 标志、类型收窄等

Low (20/20 完成):
- 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等)
- nanoid 统一替代 Date.now()+Math.random()
- confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等

返工审计修复 (10/10 完成):
- L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert
- M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死
- M-42: 脱敏短值(length <= 4)泄露修复
- M-47: tasks:update 补全 title/description 类型校验
- L-9: ollama.adapter 非流式路径 nanoid 统一
- M-45: audit:query limit 策略与 memory:listAll 一致化
- SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup
- L-15: CommandPalette useMemo 补全 sessions 响应式依赖
- useAgentStream 事件类型补全 seq/timestamp 字段

新增依赖: shell-quote + @types/shell-quote
版本号: 0.3.0 -> 0.3.1
This commit is contained in:
thzxx
2026-07-13 22:36:58 +08:00
parent 4f5f570ac8
commit e4d81d8247
47 changed files with 2247 additions and 475 deletions
+123 -5
View File
@@ -8,13 +8,17 @@
* 2. 通过 SandboxManager.validatePath 校验工作目录
* 3. 命令注入模式检测扩展
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具
* @see standard/开发规范.md — 使用 shell-quote 解析命令(禁止简单字符串匹配
* C-4 修复(v0.3.1):双层命令注入防御
* 1. 主层 — 使用 shell-quote 解析命令为 token 数组,对每个 token 做模式匹配
* 2. 补充层 — 保留原正则检测作为 fallback,覆盖 Windows cmd 语法
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具 + run_command 安全规则
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import { resolve } from 'path';
import { parse as shellQuoteParse } from 'shell-quote';
import log from 'electron-log';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
@@ -161,8 +165,13 @@ export class RunCommandTool implements IMetonaTool {
/**
* 命令安全校验
*
* 使用模式匹配检查危险命令。
* C-4 修复: 采用双层防御
* 1. 主层 — 使用 shell-quote 解析命令为 token 数组,对每个 token 做模式匹配
* 可防御所有字符串拼接绕过(如 `r"m" -rf /`、`$'rm'`、`$(echo rm)`、`r''m`
* 2. 补充层 — 保留原正则检测作为 fallback,覆盖 Windows cmd 语法和 shell-quote 无法解析的场景
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — run_command 安全规则
* @see standard/开发规范.md — 使用 shell-quote 解析命令(禁止简单字符串匹配)
*/
private validateCommand(command: string): { allowed: boolean; reason?: string } {
const cmd = command.trim().toLowerCase();
@@ -172,8 +181,16 @@ export class RunCommandTool implements IMetonaTool {
return { allowed: false, reason: 'Access denied: MEMORY.md is managed by the memory system and cannot be accessed via command execution' };
}
// 硬阻止列表(绝对禁止执行)
// v0.2.0: 扩展危险命令检测模式
// ===== 主层: shell-quote token-level 检测 =====
// 解析失败(Windows cmd 语法等)时降级到正则补充层
const tokenBlock = this.checkTokens(command);
if (tokenBlock !== null) return tokenBlock;
// ===== 补充层: 原正则检测(保留所有原模式) =====
// 覆盖 shell-quote 无法解析的场景:
// - Windows cmd 语法(&、|、>nul、chcp 等)
// - 复杂管道序列(curl ... | sh
// - Fork bomb 等特殊语法
const hardBlocks = [
// 文件系统破坏
{ pattern: /\brm\b.*\//, reason: 'rm with absolute path is forbidden' },
@@ -218,4 +235,105 @@ export class RunCommandTool implements IMetonaTool {
return { allowed: true };
}
/**
* C-4 修复: shell-quote token-level 安全检测
*
* 将命令解析为 token 数组,提取所有命令名和参数(忽略 shell 运算符),
* 对每个 token 做精确匹配。可防御所有字符串拼接绕过:
* - `r"m" -rf /` → 解析为 ['rm', '-rf', '/'] → 命中 rm 检测
* - `$'rm'` → 解析为 ['rm'] → 命中 rm 检测
* - `$(echo rm) -rf /` → 命令替换会被 shell-quote 识别为运算符序列
*
* @returns null 表示通过检测;非 null 表示被阻止(含 reason
*/
private checkTokens(command: string): { allowed: boolean; reason?: string } | null {
let tokens: ReturnType<typeof shellQuoteParse>;
try {
tokens = shellQuoteParse(command);
} catch {
// 解析失败(Windows cmd 语法、不完整的引号等)— 降级到正则补充层
return null;
}
// 提取所有 word token(命令名和参数),忽略运算符(&&、|、; 等)
// 同时跟踪管道运算符,检测危险组合(如 `| sh`)
const words: string[] = [];
let prevWasPipe = false;
for (const entry of tokens) {
if (typeof entry === 'string') {
// 裸字符串 token — 若前一 token 是管道,检测是否为 sh/bash(远程代码执行)
if (prevWasPipe && /^(ba)?sh$/i.test(entry)) {
return { allowed: false, reason: 'Remote code execution via pipe is forbidden' };
}
words.push(entry);
prevWasPipe = false;
} else if (typeof entry === 'object' && entry !== null) {
const obj = entry as { word?: unknown; op?: unknown };
if (typeof obj.word === 'string') {
// 引号包裹或变量替换后的 token
if (prevWasPipe && /^(ba)?sh$/i.test(obj.word)) {
return { allowed: false, reason: 'Remote code execution via pipe is forbidden' };
}
words.push(obj.word);
prevWasPipe = false;
} else if (typeof obj.op === 'string') {
// 跟踪管道运算符,用于下一轮检测 `| sh`
prevWasPipe = (obj.op === '|');
}
}
}
// 危险命令名 token(精确匹配,大小写不敏感)
const dangerousCommands = new Set([
'sudo', 'su', 'doas',
'shutdown', 'reboot', 'halt', 'poweroff',
'mkfs', 'fdisk', 'format', 'diskpart',
]);
// 危险参数 token
const dangerousArgs = new Set([
'-enc', '-encodedcommand', // PowerShell 编码执行
]);
for (const word of words) {
const lower = word.toLowerCase();
// 1. 危险命令名(精确匹配,或 mkfs/fdisk 前缀匹配如 mkfs.ext4
if (dangerousCommands.has(lower) || /^(mkfs|fdisk)\b/.test(lower)) {
if (['sudo', 'su', 'doas'].includes(lower)) {
return { allowed: false, reason: 'Privilege escalation commands are forbidden' };
}
if (['shutdown', 'reboot', 'halt', 'poweroff'].includes(lower)) {
return { allowed: false, reason: 'System shutdown commands are forbidden' };
}
// mkfs/fdisk/format/diskpart + mkfs.ext4 等前缀匹配
return { allowed: false, reason: 'Disk formatting commands are forbidden' };
}
// 2. 危险参数(精确匹配)
if (dangerousArgs.has(lower)) {
return { allowed: false, reason: 'PowerShell encoded command execution is forbidden' };
}
// 3. dd 写设备文件:of=/dev/...(不依赖系统目录前置,独立检测)
if (/^of=\/dev\//.test(lower)) {
return { allowed: false, reason: 'Writing to device files is forbidden' };
}
}
// 4. 检测 rm 与系统目录的组合(token 序列检测)
// 例如: ['rm', '-rf', '/etc'] 应被拦截
let hasRm = false;
let hasSystemPath = false;
for (const word of words) {
const lower = word.toLowerCase();
if (lower === 'rm') hasRm = true;
if (/^\/(?:bin|boot|dev|etc|lib|proc|root|sbin|sys|usr|var)\b/.test(lower)) {
hasSystemPath = true;
}
}
if (hasRm && hasSystemPath) {
return { allowed: false, reason: 'rm on system directories is forbidden' };
}
return null;
}
}