feat: 升级至 v0.3.16 — 工单修复 51 项 + 审查修复 30 项(安全加固/适配器/工具系统/数据层/前端)
This commit is contained in:
@@ -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) };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user