Files
Nexus/docs/audit/2026-06-01-files-page-row-click.md
T

8.0 KiB
Raw Blame History

审计记录:文件管理行点击与自动选服

审计信息

  • 日期: 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 ☑ 已审

变更摘要(审计对象)

  1. Vuetify 4 兼容:移除无效的 @click:row / @dblclick:row / @contextmenu:row,改用 :row-props="fileRowProps" 绑定 onClick / onDblclick / onContextmenu
  2. 自动选服onMountedloadServers() 后调用 resolveInitialServerId()URL → sessionStorage → 第一台),并 browse()
  3. 记忆服务器onServerChange 写入 sessionStoragenexus:files:last_server_id
  4. 列表加载useServerList 改用 http.getList,兼容数组与 { items, total }

审计 8 步结果

Step 1: 登记

已登记 3 个文件;变更动机与 changelog 一致。

Step 2: 全文 Read

  • FilesPage.vue:template(工具栏、表格、对话框、FileEditorWorkbench+ script setup 全量通读。
  • useServerList.ts:全量 37 行。
  • 交叉引用:remotePath.tsfileBrowse.tsapi/index.tsgetList)未改,仅核对调用契约。

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.itemFileEntry 一致
H12 常量声明顺序 FILES_LAST_SERVER_KEYonServerChange 之后 见 Closure — 运行时无 TDZ 问题,可读性欠佳

Step 4: Closure 表

H 判定 依据
H1 通过 v-html{{ item.name }} 等文本插值
H2 通过 sessionStorageString(serverId)
H3 通过 resolveInitialServerId L857-868Number.isFinite + serverList.someAPI 仍由后端鉴权
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-801navigateToPath 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 变更原因
  • useServerListSchedulesPage/RetriesPage 列表解析策略统一
  • 常量位置可优化(H12 建议)⚠️

结论

审计通过,0 FINDING,可合并/可部署

部署前提醒(非代码缺陷):

  1. Windows tar-C web/app 可能失败,请用 WSL 或 deploy/deploy-frontend.sh 打包上传。
  2. 生产 prunecd /www/wwwroot/api.synaglobal.vip && python3 deploy/prune_frontend_assets.py
  3. 浏览器验证 #/files 后建议 Ctrl+F5 强刷。

可选后续小改(不阻断):

  • FILES_LAST_SERVER_KEY 移至 state 区顶部。
  • watch(route.query.server_id) 增加与 resolveInitialServerId 相同的列表校验。