feat: 升级至 v0.3.16 — 工单修复 51 项 + 审查修复 30 项(安全加固/适配器/工具系统/数据层/前端)

This commit is contained in:
2026-07-22 10:31:13 +08:00
parent 516d8a728f
commit 2c2ca4a692
43 changed files with 1454 additions and 301 deletions
+110 -17
View File
@@ -15,7 +15,7 @@
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具 + run_command 安全规则
*/
import { exec } from 'child_process';
import { exec, execFile } from 'child_process';
import { promisify } from 'util';
import { resolve } from 'path';
import { parse as shellQuoteParse } from 'shell-quote';
@@ -27,6 +27,7 @@ import { commandTouchesProtectedFile, isPathWithinWorkspace } from './file-guard
import type { SandboxManager } from '../../sandbox/sandbox';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
// ===== Windows 编码智能解码 =====
// 某些 Windows 程序(dir、systeminfo、ipconfig 等)不响应 chcp 65001
@@ -48,6 +49,43 @@ function decodeBuffer(buf: Buffer): string {
}
}
/**
* #9 修复 + 审查修复: 构建安全的子进程环境变量
*
* 审查修复: 原白名单方案遗漏了 GIT_*/PYTHONPATH/HTTP_PROXY
*
*/
function buildSafeCommandEnv(isWindows: boolean): Record<string, string> {
// 敏感变量后缀黑名单
const SENSITIVE_SUFFIXES = [
'_API_KEY', '_TOKEN', '_SECRET', '_PASSWORD', '_PASSWD',
'_CREDENTIAL', '_CREDENTIALS', '_PRIVATE_KEY',
];
// 敏感变量名黑名单(精确匹配)
const SENSITIVE_KEYS = new Set([
'DEEPSEEK_API_KEY', 'AGNES_API_KEY', 'MIMO_API_KEY',
'GITEA_PASSWORD', 'DATABASE_PASSWORD',
]);
const env: Record<string, string> = {};
for (const [key, val] of Object.entries(process.env)) {
if (!val) continue;
if (SENSITIVE_KEYS.has(key)) continue;
if (SENSITIVE_SUFFIXES.some((suffix) => key.toUpperCase().endsWith(suffix))) continue;
env[key] = val;
}
// 添加必要的运行时变量
env.NODE_ENV = 'production';
if (isWindows) {
env.PYTHONIOENCODING = 'utf-8';
env.LANG = 'zh_CN.UTF-8';
env.LC_ALL = 'zh_CN.UTF-8';
}
return env;
}
// ===== 9. run_command =====
export class RunCommandTool implements IMetonaTool {
@@ -120,28 +158,46 @@ export class RunCommandTool implements IMetonaTool {
// Windows 中文编码修复:通过 chcp 65001 切换控制台到 UTF-8 代码页
// 避免 GBK/CP936 输出被 Node.js 按 UTF-8 解码导致中文乱码
const isWindows = process.platform === 'win32';
const finalCommand = isWindows
? `chcp 65001 >nul 2>&1 && ${command}`
: command;
// #8 修复: 优先使用 execFile(不经过 shell,从根本上防止命令注入)
// 仅当命令为简单命令(无管道、重定向、&& 等 shell 运算符)时使用 execFile
// 复杂命令(含 shell 语法)降级到 exec(已有 SandboxManager + validateCommand 双层校验)
const simpleCmd = this.parseCommandSimple(command);
// 使用 encoding: 'buffer' 获取原始字节,手动智能解码
// 部分程序(如 dir、systeminfo)不响应 chcp 65001,仍输出 GBK 字节流
const { stdout, stderr } = await execAsync(finalCommand, {
// #9 修复: 不透传完整 process.env,仅保留子进程运行所需的最小环境变量集合
// 防止 API keys / tokens / passwords 通过环境变量泄露给子进程
const execEnv = buildSafeCommandEnv(isWindows);
const execOpts = {
cwd: resolvedWorkdir,
timeout,
maxBuffer: 1024 * 1024, // 1MB
encoding: 'buffer', // 返回 Buffer 而非字符串,便于智能解码
env: {
...process.env,
NODE_ENV: 'production',
...(isWindows ? {
// 强制常见程序使用 UTF-8 输出
PYTHONIOENCODING: 'utf-8',
LANG: 'zh_CN.UTF-8',
LC_ALL: 'zh_CN.UTF-8',
} : {}),
},
});
encoding: 'buffer' as const, // 返回 Buffer 而非字符串,便于智能解码
env: execEnv,
};
let stdout: Buffer;
let stderr: Buffer;
// #8 修复 + 审查修复: 简单命令使用 execFile(不经过 shell,防止命令注入)
// 但 Windows 上 npm/npx/yarn/pnpm/tsc 等是 .cmd 批处理,execFile 无法执行(ENOENT
// 因此 Windows 上仍用 exec(已有 SandboxManager.scanCode + validateCommand 双层校验)
// 非 Windows 上对简单命令用 execFile
if (simpleCmd && !isWindows) {
const result = await execFileAsync(simpleCmd.command, simpleCmd.args, execOpts);
stdout = result.stdout;
stderr = result.stderr;
} else {
// 复杂命令(含管道/重定向/&& 等 shell 语法)或 Windows — 使用 exec
// 已有 SandboxManager.scanCode + validateCommand 双层安全校验
const finalCommand = isWindows
? `chcp 65001 >nul 2>&1 && ${command}`
: command;
const result = await execAsync(finalCommand, execOpts);
stdout = result.stdout;
stderr = result.stderr;
}
return {
success: true,
@@ -336,4 +392,41 @@ export class RunCommandTool implements IMetonaTool {
return null;
}
/**
* #8 修复: 将命令解析为简单的 command + args 数组
*
* 使用 shell-quote 解析命令,如果命令只包含 word tokens(无管道、重定向、&& 等 shell 运算符),
* 返回 { command, args } 供 execFile 使用(不经过 shell,从根本上防止命令注入)。
* 如果包含 shell 运算符或解析失败,返回 null,调用方降级到 exec(已有安全校验)。
*
* @returns 简单命令的 { command, args },或 null 表示复杂命令
*/
private parseCommandSimple(command: string): { command: string; args: string[] } | null {
let tokens: ReturnType<typeof shellQuoteParse>;
try {
tokens = shellQuoteParse(command);
} catch {
// 解析失败(Windows cmd 语法、不完整引号等)— 降级到 exec
return null;
}
const words: string[] = [];
for (const entry of tokens) {
if (typeof entry === 'string') {
words.push(entry);
} else if (typeof entry === 'object' && entry !== null) {
const obj = entry as { word?: unknown; op?: unknown };
if (typeof obj.word === 'string') {
words.push(obj.word);
} else if (typeof obj.op === 'string') {
// 遇到 shell 运算符(&&、|、;、> 等)— 复杂命令,降级到 exec
return null;
}
}
}
if (words.length === 0) return null;
return { command: words[0], args: words.slice(1) };
}
}