Files
metona-ollama-desktop/src/main/tool-security.ts
T
紫影233 042e00e4a6 v0.12.3: 全面审计修复 + 稳定性加固 + 文档更新
P0 安全/稳定性修复 (6项):
- sqlite persist 改用临时文件+rename,I/O错误日志
- IPC fs 处理器加入 checkPathAllowed 路径验证
- handleCompress 命令注入修复 + 跨平台 (powershell/tar/zip/unzip)
- browser 全页截图简化,使用 capturePage full rect
- workspace 竞态条件修复
- editFile 5MB 大小限制

P1 安全/稳定性修复 (7项):
- 大小写不敏感搜索: query 同步转换小写
- web_fetch 无 content-length 时流式读取+10MB限制防OOM
- Windows SIGTERM 改用 taskkill /PID /T /F 强制终止
- startsWith 路径检查 Windows 大小写不敏感
- 安全黑名单扩展: 33个目录路径 + 30条危险命令 (Linux+Windows)
- plan_track stepIndex 改用 typeof === 'number' 精确检查
- MCP 前缀改用双下划线分隔 mcp_{server}__{tool} 防歧义

P2 代码质量修复 (8项):
- 修复 EXECUTING→THINKING 死循环Bug (转换表缺失)
- COMPRESSING 硬上限绕过时同步 _loopState 全局状态
- Abort 路径补齐 onDone 回调, 防止UI状态残留
- 幻觉检测从4条扩展到16条规则, 覆盖全部工具类别
- 移除 getTokenEfficiency 死代码
- 跨平台路径清理 (replace(/[\/]+$/, ''))
- 动态 import 加 try/catch, 失败自动批准不阻断
- #modelSelect 加 null 守卫, 防止运行时崩溃

文档 & UI 更新:
- README.md: 版本号/特性表/架构图/安全机制/工具清单全面刷新
- 帮助面板: 新增 Plan Mode、抗幻觉&稳定性章节, Agent Loop补8状态机
- 工具面板: plan_track卡片 + plan-mode徽章样式, 标题更新为42+1
- 工作空间面板宽度: 480px → 400px
- 源码注释移除所有版本标签, 仅保留4处必要位置
- 版本号 0.12.2 → 0.12.3
2026-06-23 17:06:43 +08:00

127 lines
3.6 KiB
TypeScript

/**
* Tool Security - 安全检查模块
* 路径白名单/黑名单、命令过滤、路径遍历检测
*/
import * as path from 'path';
import * as os from 'os';
const HOME = os.homedir();
/** 默认允许的目录(可通过设置覆盖) */
let allowedDirs: string[] = [
HOME,
path.join(HOME, 'Desktop'),
path.join(HOME, 'Documents'),
path.join(HOME, 'Downloads'),
path.join(HOME, 'Projects'),
path.join(HOME, 'projects'),
'/tmp',
];
/** 永久禁止的目录 */
const BLOCKED_DIRS: string[] = [
// Linux/macOS 系统目录
'/etc', '/sys', '/proc', '/dev', '/boot', '/root',
'/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/lib',
'/var/log', '/var/run', '/var/spool', '/var/mail',
'/tmp/.X11-unix', '/tmp/.font-unix',
// Windows 系统目录
'C:\\Windows', 'C:\\Windows\\System32', 'C:\\Windows\\SysWOW64',
'C:\\Windows\\System', 'C:\\Windows\\WinSxS',
'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\ProgramData',
// 用户敏感目录
path.join(HOME, '.ssh'),
path.join(HOME, '.gnupg'),
path.join(HOME, '.aws'),
path.join(HOME, '.azure'),
path.join(HOME, '.config'),
path.join(HOME, '.kube'),
path.join(HOME, 'AppData'),
];
/** 命令黑名单 */
const BLOCKED_COMMANDS: string[] = [
// POSIX 危险命令
'rm -rf /', 'rm -rf /*', ':(){ :|:& };:',
'mkfs', 'dd if=', 'wipefs', 'shred',
'shutdown', 'reboot', 'poweroff', 'halt',
'useradd', 'usermod', 'userdel', 'passwd',
'chmod 777', 'chown root',
'crontab -e',
'systemctl enable', 'systemctl disable',
'curl | sh', 'wget | sh', 'curl | bash', 'wget | bash',
'eval', 'exec >',
// Windows 危险命令
'rmdir /s', 'del /f', 'del /s', 'del /q',
'format', 'diskpart',
'reg add', 'reg delete', 'reg import', 'reg export',
'sc stop', 'sc delete', 'sc config',
'net user', 'net localgroup', 'net share',
'icacls', 'takeown', 'cacls',
'bcdedit', 'wmic', 'rundll32',
'schtasks /create', 'schtasks /delete',
];
export interface CheckResult {
ok: boolean;
reason?: string;
}
export function checkPathAllowed(targetPath: string, operation: 'read' | 'write'): CheckResult {
const resolved = path.resolve(targetPath);
if (targetPath.split(path.sep).filter(s => s === '..').length > 5) {
return { ok: false, reason: '路径遍历深度过大' };
}
for (const blocked of BLOCKED_DIRS) {
if (resolved === blocked || resolved.startsWith(blocked + path.sep)) {
return { ok: false, reason: `禁止访问受保护路径: ${blocked}` };
}
}
if (operation === 'write') {
const inAllowedDir = allowedDirs.some(dir =>
resolved === dir || resolved.startsWith(dir + path.sep)
);
if (!inAllowedDir) {
return { ok: false, reason: `写操作被限制在允许的目录内。当前路径: ${resolved}` };
}
}
return { ok: true };
}
export function checkCommandAllowed(command: string): CheckResult {
const lowerCmd = command.toLowerCase().trim();
for (const blocked of BLOCKED_COMMANDS) {
if (lowerCmd.includes(blocked.toLowerCase())) {
return { ok: false, reason: `命令包含被禁止的操作: ${blocked}` };
}
}
if (/(\||>|<)\s*(sh|bash|zsh|powershell|cmd)/i.test(lowerCmd)) {
return { ok: false, reason: '禁止通过管道执行 shell 命令' };
}
if (/\/dev\/tcp\//i.test(lowerCmd) || /bash\s+-i\s+>&/i.test(lowerCmd)) {
return { ok: false, reason: '检测到疑似反弹 shell 操作' };
}
return { ok: true };
}
export function setAllowedDirs(dirs: string[]): void {
allowedDirs = dirs.map(d => path.resolve(d));
}
export function getAllowedDirs(): string[] {
return [...allowedDirs];
}
export function getBlockedDirs(): string[] {
return [...BLOCKED_DIRS];
}