v0.3.3: 代码安全审查与Bug修复

CRITICAL 修复:
- 路径遍历防护绕过:validatePath在normalize()之后检查..,可被绕过读取任意文件
- 编辑器异步创建竞态条件:editor.create()完成后组件已卸载导致内存泄漏
- editor.create() Promise缺少.catch() → 未捕获的Promise拒绝
- handleOpenRecent空文件被跳过:result.content truthy检查阻塞空文件打开

MAJOR 修复:
- DIR_WATCH/TAB_SWITCHED IPC缺少路径校验,可被用于监视任意文件
- AboutDialog内联箭头函数使React.memo完全失效
- ConfirmDialog关闭时焦点恢复逻辑反转
- markdown.ts一刀切屏蔽..导致合法相对路径图片不显示

安全加固:
- 移除未被使用的path.normalize导入
- 所有文件操作IPC使用validatePath统一校验
- 编辑器初始化添加cancelled标志防止卸载后设置实例

验证: TypeScript零错误, ESLint零错误零警告, 78/78测试通过
This commit is contained in:
thzxx
2026-06-04 13:02:19 +08:00
parent 8bcd8a2b2b
commit efa7f61809
18 changed files with 72 additions and 33 deletions
+10 -5
View File
@@ -3,18 +3,18 @@ import { readFileContent, saveFileContent, buildDirTree } from './file-system'
import { FileWatcher, SidebarWatcher } from './file-watcher'
import { IPC_CHANNELS } from '../shared/ipc-channels'
import { stat } from 'fs/promises'
import { basename, normalize, isAbsolute } from 'path'
import { basename, isAbsolute } from 'path'
// 安全校验:拒绝路径遍历攻击
function validatePath(filePath: string): boolean {
if (!filePath || typeof filePath !== 'string') return false
// 拒绝空字节
if (filePath.includes('\0')) return false
// 规范化路径后检查是否包含 ..
const normalized = normalize(filePath)
if (normalized.includes('..')) return false
// 在 normalize 之前检查原始路径中的 .. 序列,防止 normalize 解析后绕过
// 例如 '/valid/path/../../etc/shadow' normalize 后变为 '/etc/shadow'.. 已消失
if (filePath.includes('..')) return false
// 必须是绝对路径
if (!isAbsolute(normalized)) return false
if (!isAbsolute(filePath)) return false
return true
}
@@ -185,6 +185,7 @@ export function registerIpcHandlers(
// 目录监听
ipcMain.handle(IPC_CHANNELS.DIR_WATCH, (_event: IpcMainInvokeEvent, dirPath: string) => {
if (!validatePath(dirPath)) return
sidebarWatcher.start(dirPath)
})
@@ -195,6 +196,10 @@ export function registerIpcHandlers(
// 标签切换
ipcMain.handle(IPC_CHANNELS.TAB_SWITCHED, (_event: IpcMainInvokeEvent, filePath: string | null) => {
state.activeFilePath = filePath || null
if (filePath && !validatePath(filePath)) {
fileWatcher.stop()
return
}
fileWatcher.start(filePath || '')
const win = getMainWindow()
if (win && !win.isDestroyed()) {