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
+48 -3
View File
@@ -22,9 +22,9 @@
* @see standard/开发规范.md — 使用 fs/path 内置模块
*/
import { readFile, writeFile, readdir, stat, mkdir, open, unlink, rmdir, rm, rename, type FileHandle } from 'fs/promises';
import { readFile, writeFile, readdir, stat, mkdir, open, unlink, rmdir, rm, rename, realpath, type FileHandle } from 'fs/promises';
import { join, relative, resolve, dirname } from 'path';
import { existsSync } from 'fs';
import { existsSync, realpathSync } from 'fs';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
@@ -542,7 +542,23 @@ export class SearchFilesTool implements IMetonaTool {
fileGlob?: string,
includeHidden?: boolean,
workspacePath?: string,
// #21 修复: 添加深度限制,防止深层嵌套目录触发栈溢出
depth: number = 0,
// #21 修复: 最大递归深度(默认 20),超过则停止递归
maxDepth: number = 20,
// #21 修复: 符号链接循环防护 — 记录已访问的 realpath,防止 A -> B -> A 死循环
visited: Set<string> = new Set(),
): Promise<void> {
// #21 修复: 深度限制 — 超过 maxDepth 立即返回,防止栈溢出
if (depth > maxDepth) return;
// #21 修复: 符号链接循环防护 — realpath 去重,防止 A -> B -> A 形成循环导致死循环
// 审查修复: 改用异步 fs.promises.realpath,避免同步 realpathSync 阻塞 Electron 主进程。
// realpath 失败(权限问题等)时回退到 dirPath 本身,保守继续。
const real = await realpath(dirPath).catch(() => dirPath);
if (visited.has(real)) return;
visited.add(real);
try {
const entries = await readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
@@ -552,7 +568,10 @@ export class SearchFilesTool implements IMetonaTool {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
await this.walkDir(fullPath, callback, fileGlob, includeHidden, workspacePath);
// #21 修复: 不跟随符号链接目录,防止循环遍历与越权访问
if (!entry.isSymbolicLink()) {
await this.walkDir(fullPath, callback, fileGlob, includeHidden, workspacePath, depth + 1, maxDepth, visited);
}
} else {
// F2-4: 支持多 glob(逗号分隔,如 "*.ts,*.js,*.tsx"
if (fileGlob && !matchAnyGlob(entry.name, fileGlob)) continue;
@@ -640,9 +659,35 @@ export class DeleteFileTool implements IMetonaTool {
}
try {
// #20 修复: TOCTOU 竞态防护 — 删除前 realpath 二次校验
// 攻击场景:在校验(safeResolvePath)与删除(unlink/rmdir)之间,
// 攻击者用符号链接替换目标文件,使 unlink 实际删除符号链接指向的工作空间外文件
// 防护:记录删除前 realpath,删除前再次校验未被替换
let realBefore: string;
try {
realBefore = realpathSync(resolvedPath);
} catch {
// realpath 失败(路径不存在等),使用 resolvedPath
realBefore = resolvedPath;
}
if (!isPathWithinWorkspace(realBefore, context.workspacePath)) {
return { error: 'Path escape detected (TOCTOU protection)', path: filePath, success: false };
}
const stats = await stat(resolvedPath);
const isDirectory = stats.isDirectory();
// #20 修复: 删除前再次 realpath 校验,确保文件未被替换为指向工作空间外的符号链接
let realAfter: string;
try {
realAfter = realpathSync(resolvedPath);
} catch {
realAfter = resolvedPath;
}
if (realAfter !== realBefore) {
return { error: 'TOCTOU detected: file replaced during deletion', path: filePath, success: false };
}
if (isDirectory) {
if (recursive) {
// 递归删除非空目录