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