67dac0168f
Co-authored-by: Cursor <cursoragent@cursor.com>
8.0 KiB
8.0 KiB
审计记录:文件管理行点击与自动选服
审计信息
- 日期: 2026-06-01
- 审计人: Claude (AI)
- 触发原因: Bug 修复 —
#/files无法进入子目录、未选服务器时空列表 - 关联 changelog:
docs/changelog/2026-06-01-files-page-row-click.md
审计范围(实际改动文件清单)
| 文件 | 行数 | 状态 |
|---|---|---|
frontend/src/pages/FilesPage.vue |
1429 | ☑ 全文已审 |
frontend/src/composables/useServerList.ts |
37 | ☑ 全文已审 |
docs/changelog/2026-06-01-files-page-row-click.md |
25 | ☑ 已审 |
变更摘要(审计对象)
- Vuetify 4 兼容:移除无效的
@click:row/@dblclick:row/@contextmenu:row,改用:row-props="fileRowProps"绑定onClick/onDblclick/onContextmenu。 - 自动选服:
onMounted在loadServers()后调用resolveInitialServerId()(URL → sessionStorage → 第一台),并browse()。 - 记忆服务器:
onServerChange写入sessionStorage键nexus:files:last_server_id。 - 列表加载:
useServerList改用http.getList,兼容数组与{ items, total }。
审计 8 步结果
Step 1: 登记 ✅
已登记 3 个文件;变更动机与 changelog 一致。
Step 2: 全文 Read ✅
FilesPage.vue:template(工具栏、表格、对话框、FileEditorWorkbench)+ script setup 全量通读。useServerList.ts:全量 37 行。- 交叉引用:
remotePath.ts、fileBrowse.ts、api/index.ts(getList)未改,仅核对调用契约。
Step 3: 规则扫描 H
| ID | 规则 | 扫描位置 | 命中 |
|---|---|---|---|
| H1 | XSS(未消毒用户输入进 DOM) | FilesPage.vue template |
否 — 文件名来自 API,Vue 默认转义 |
| H2 | 凭据泄露 | sessionStorage、路由 query | 否 — 仅存 server_id 数字 |
| H3 | IDOR(越权 server_id) | browse()、resolveInitialServerId |
否 — 后端 JWT + get_by_id;前端校验 ID 在列表内(初始化路径) |
| H4 | 路径穿越(../) |
openEntry 符号链接绝对路径 |
否(既有)— normalizeRemotePath;穿越依赖 SSH 返回的 link_target,属管理员可信面 |
| H5 | 静默吞错 | useServerList catch |
既有模式 — servers=[],页面有空状态提示 |
| H6 | 事件委托冲突(checkbox/按钮误触发行导航) | fileRowProps |
已缓解 — isRowActionTarget + closest |
| H7 | Vuetify API 废弃 | @click:row |
已修复 — 改用 row-props |
| H8 | 未选服务器调用 API | browse() |
否 — 入口 if (!selectedServer.value) return |
| H9 | 双击/单击竞态 | 目录行 click + dblclick | 否 — onRowDblClick 仅处理 file 类型 |
| H10 | sessionStorage 污染 | FILES_LAST_SERVER_KEY |
否 — 写入前 onServerChange 有值;读取时 serverList.some 校验 |
| H11 | 类型安全 | fileRowProps(data) |
可接受 — data.item 与 FileEntry 一致 |
| H12 | 常量声明顺序 | FILES_LAST_SERVER_KEY 在 onServerChange 之后 |
见 Closure — 运行时无 TDZ 问题,可读性欠佳 |
Step 4: Closure 表
| H | 判定 | 依据 |
|---|---|---|
| H1 | 通过 | 无 v-html;{{ item.name }} 等文本插值 |
| H2 | 通过 | sessionStorage 仅 String(serverId) |
| H3 | 通过 | resolveInitialServerId L857-868:Number.isFinite + serverList.some;API 仍由后端鉴权 |
| H4 | 通过(既有) | 符号链接绝对路径 L751-752 走 normalizeRemotePath;后端 sync_v2.browse_directory 亦有规范化 |
| H5 | 通过(既有) | useServerList L29-30 与多页一致;Files 页 onMounted L1363-1365 有 snackbar |
| H6 | 通过 | isRowActionTarget L891-894 排除 .v-selection-control, .v-btn, … |
| H7 | 通过 | L187 :row-props="fileRowProps";L896-914 实现与 Vuetify 4 RowPropsFunction 一致 |
| H8 | 通过 | browse L800-801;navigateToPath L759-762 |
| H9 | 通过 | onRowDblClick L923-926 仅 isRegularFile |
| H10 | 通过 | L861-866、L844-845 |
| H11 | 通过 | TypeScript type-check 已通过(本地构建前) |
| H12 | 建议 | FILES_LAST_SERVER_KEY 宜上移至 // ── State ── 区,非功能缺陷 |
附带观察(非本次引入,不记 FINDING):
watch(route.query.server_id)L1378-1390 未校验serverList,非法?server_id=abc可能得到NaN(原逻辑已存在)。建议在后续小改中与resolveInitialServerId对齐。
Step 5: 入口表
| 入口类型 | 位置 | 说明 |
|---|---|---|
| 用户点击 | fileRowProps.onClick/onDblclick/onContextmenu |
行级导航/编辑/菜单 |
| 用户选择 | v-select selectedServer |
切换服务器 → onServerChange |
| 路由 query | server_id, path |
onMounted / watch / syncRouteQuery |
| sessionStorage | nexus:files:last_server_id |
恢复上次服务器 |
| HTTP GET | /servers/ |
useServerList.loadServers |
| HTTP POST | /sync/browse 等 |
既有文件操作(未改契约) |
Step 6: 输入 → Sink
[URL server_id / sessionStorage / 默认第一台]
→ resolveInitialServerId (校验 ∈ serverList)
→ selectedServer
→ browse() → POST /sync/browse { server_id, path }
→ 后端 JWT + ServerRepository + SSH ls
[行点击 item.name]
→ openEntry → joinRemotePath + normalizeRemotePath
→ browse()(同上)
[行点击 非目录]
→ onRowClick 无操作(仅目录/符号链接)
[双击文件]
→ editFile → readRemoteFileContent → FileEditorWorkbench
控制措施:路径片段经 joinRemotePath/validatePathSegment;服务器 ID 初始化路径白名单校验;API 鉴权在后端。
Step 7: 归类
FilesPage.vue 1429行 12H 0 FINDING
useServerList.ts 37行 1H 0 FINDING
changelog 25行 0H 0 FINDING
────────────────────────────────────────────
总计 13H 0 FINDING
Step 8: DoD ✅
| 维度 | 项 | 结果 |
|---|---|---|
| 安全 | 无新增 XSS/IDOR/凭据泄露 | ✅ |
| 逻辑 | 目录单击可进入;无 server 时自动选服并 browse | ✅ |
| 逻辑 | 复选框/操作列不误触发目录进入 | ✅ |
| 性能 | row-props 每行创建闭包(与 Vuetify 常规用法一致) |
✅ |
| 质量 | TS type-check | ✅(实现时已通过) |
| 质量 | FILES_LAST_SERVER_KEY 声明位置 |
⚠️ 建议上移(不阻断) |
| 文档 | changelog ≥10 行 | ✅ |
| 部署 | 生产 tar 需在 WSL/Git Bash 重跑;prune 路径为仓库根 deploy/prune_frontend_assets.py |
⚠️ 运维注意 |
FINDING 列表
无(0 FINDING)。
审查 4 维度(摘要)
安全(12 项抽样)
- JWT 请求经既有
http客户端 ✅ - 不在前端存储 SSH 密码 ✅
server_id仅数字、列表内校验(初始化) ✅- 行事件不执行
eval/动态 HTML ✅
逻辑(8 项)
- 根因:Vuetify 4 无
click:row→ 已用row-props修复 ✅ - 根因:无 query 不 browse → 已自动选服 ✅
- 空服务器列表提示 ✅
- 符号链接相对路径不盲目 navigate ✅
性能(5 项)
displayedFiles仍为 computed 过滤,未增加额外请求 ✅preloadDirectoryFiles行为未改 ✅
质量(5 项)
- 注释说明 Vuetify 4 变更原因 ✅
useServerList与SchedulesPage/RetriesPage列表解析策略统一 ✅- 常量位置可优化(H12 建议)⚠️
结论
☑ 审计通过,0 FINDING,可合并/可部署
部署前提醒(非代码缺陷):
- Windows
tar对-C web/app可能失败,请用 WSL 或deploy/deploy-frontend.sh打包上传。 - 生产 prune:
cd /www/wwwroot/api.synaglobal.vip && python3 deploy/prune_frontend_assets.py - 浏览器验证
#/files后建议 Ctrl+F5 强刷。
可选后续小改(不阻断):
- 将
FILES_LAST_SERVER_KEY移至 state 区顶部。 watch(route.query.server_id)增加与resolveInitialServerId相同的列表校验。