184 lines
8.0 KiB
Markdown
184 lines
8.0 KiB
Markdown
|
|
# 审计记录:文件管理行点击与自动选服
|
|||
|
|
|
|||
|
|
## 审计信息
|
|||
|
|
|
|||
|
|
- **日期**: 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. **自动选服**:`onMounted` 在 `loadServers()` 后调用 `resolveInitialServerId()`(URL → sessionStorage → 第一台),并 `browse()`。
|
|||
|
|
3. **记忆服务器**:`onServerChange` 写入 `sessionStorage` 键 `nexus: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.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,可合并/可部署**
|
|||
|
|
|
|||
|
|
**部署前提醒**(非代码缺陷):
|
|||
|
|
|
|||
|
|
1. Windows `tar` 对 `-C web/app` 可能失败,请用 WSL 或 `deploy/deploy-frontend.sh` 打包上传。
|
|||
|
|
2. 生产 prune:`cd /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` 相同的列表校验。
|