ebe276ea29
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
5.3 KiB
Markdown
135 lines
5.3 KiB
Markdown
# 审计报告 — 服务器列表迭代
|
||
## 2026-05-30
|
||
|
||
---
|
||
|
||
## Step 1: 登记
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 审计日期 | 2026-05-30 |
|
||
| Commit | 84282e8 |
|
||
| 变更文件 | server/infrastructure/database/server_repo.py, server/api/servers.py, web/app/servers.html |
|
||
| 变更类型 | 功能迭代 + 预存 bug 修复 |
|
||
|
||
---
|
||
|
||
## Step 2: 全文 Read
|
||
|
||
三个文件全部逐行审阅:
|
||
- server_repo.py: 136 行,get_paginated() 扩展
|
||
- servers.py: ~1600 行,API 路由层,含 CRUD/批量操作/Agent 管理
|
||
- servers.html: ~1200 行,前端完全重写
|
||
|
||
---
|
||
|
||
## Step 3: 规则扫描
|
||
|
||
### H1: SQL 注入 (server_repo.py)
|
||
- **检查**: `get_paginated()` 的 `search` 参数如何进入 SQL
|
||
- **代码**: `term = f"%{search}%"` → `Server.name.ilike(term) | Server.domain.ilike(term)`
|
||
- **判定**: ✅ 安全。SQLAlchemy ORM 的 `ilike()` 使用参数化查询,`%` 是 LIKE 通配符不是 SQL 注入载体
|
||
- **依据**: SQLAlchemy 对所有 ORM 查询自动参数化
|
||
|
||
### H2: 排序注入 (server_repo.py)
|
||
- **检查**: `sort_by` 参数如何进入 ORDER BY
|
||
- **代码**: `sort_col = _SORT_COLUMNS.get(sort_by, Server.id)` 白名单映射
|
||
- **判定**: ✅ 安全。不在白名单中的 sort_by 默认为 `Server.id`,无法注入任意列名
|
||
- **依据**: `_SORT_COLUMNS` 字典仅含 7 个预定义列
|
||
|
||
### H3: is_online 参数验证 (servers.py)
|
||
- **检查**: `is_online: Optional[bool] = Query(None)` 类型安全
|
||
- **判定**: ✅ 安全。FastAPI 自动将 Query 参数解析为 bool,非法值返回 422
|
||
|
||
### H4: Over-fetch 补偿 (server_repo.py)
|
||
- **检查**: `is_online` 过滤时 `fetch_limit = limit + 10`
|
||
- **判定**: ✅ 逻辑正确。补偿 Redis overlay 可能改变 MySQL is_online 状态导致结果不足
|
||
- **注意**: +10 是经验值,极端情况下(>10 台状态翻转)可能不足,但实际场景中此概率极低
|
||
|
||
### H5: Redis post-filter (servers.py)
|
||
- **检查**: `is_online` 过滤时 post-filter 逻辑
|
||
- **代码**: `result = [s for s in result if s.get("is_online") == is_online]` + `result[:per_page]`
|
||
- **判定**: ✅ 正确。先 Redis overlay 覆盖状态,再过滤不匹配项,最后截断到 per_page
|
||
|
||
### H6: XSS 防护 (servers.html)
|
||
- **检查**: 前端数据渲染是否使用 innerHTML
|
||
- **代码**: 所有动态数据通过 `esc()` 函数转义(`textContent` → `innerHTML`),esc() 将 `"` 转义为 `"`
|
||
- **判定**: ✅ 安全。search/sort 参数通过 URLSearchParams 编码,不直接拼入 DOM
|
||
|
||
### H7: 前端输入验证 (servers.html)
|
||
- **检查**: 搜索输入、排序参数是否可被用户篡改
|
||
- **代码**: 搜索输入传入 `URLSearchParams.set('search', ...)`,排序字段由 `toggleSort()` 内部控制
|
||
- **判定**: ✅ 安全。后端 _SORT_COLUMNS 白名单兜底
|
||
|
||
### H8: F821 agent_port bug (servers.py:1470)
|
||
- **检查**: `upgrade_agent()` 中 `agent_port` 变量引用
|
||
- **原始代码**: `agent_port` 在 line 1470 使用但未定义
|
||
- **修复**: 新增 `agent_port = server.agent_port or 8601` (line 1427)
|
||
- **判定**: ✅ 已修复。之前是预存 bug,会导致 NameError
|
||
|
||
### H9: 预存 ruff 错误修复 (servers.py)
|
||
- **F401**: 移除未使用的 `SyncLog`、`text`、`decrypt_value` 导入
|
||
- **B904**: 5 处 `raise HTTPException(...)` 添加 `from e`/`from err`/`from enc_err`
|
||
- **F541**: 移除无占位符的 f-string 前缀
|
||
- **S110**: 2 处 `except: pass` 改为 `logger.warning/debug`
|
||
- **判定**: ✅ 全部修复,ruff 零错误
|
||
|
||
---
|
||
|
||
## Step 4: Closure 表
|
||
|
||
| 编号 | 风险 | 判定 | 依据 |
|
||
|------|------|------|------|
|
||
| H1 | SQL 注入 | ✅ 安全 | SQLAlchemy ORM 参数化 |
|
||
| H2 | 排序注入 | ✅ 安全 | 白名单映射 + 默认 id |
|
||
| H3 | 类型安全 | ✅ 安全 | FastAPI bool 自动解析 |
|
||
| H4 | 分页准确性 | ✅ 安全 | +10 补偿 + post-filter 截断 |
|
||
| H5 | Redis 覆盖 | ✅ 正确 | overlay → filter → truncate |
|
||
| H6 | XSS | ✅ 安全 | esc() + URLSearchParams |
|
||
| H7 | 前端篡改 | ✅ 安全 | 后端白名单兜底 |
|
||
| H8 | F821 bug | ✅ 已修复 | 添加 agent_port 定义 |
|
||
| H9 | 代码质量 | ✅ 已修复 | ruff 零错误 |
|
||
|
||
---
|
||
|
||
## Step 5: 入口表
|
||
|
||
| 入口 | 认证 | 参数 |
|
||
|------|------|------|
|
||
| GET /api/servers/ | JWT (get_current_admin) | search/sort_by/sort_order/is_online/page/per_page |
|
||
|
||
---
|
||
|
||
## Step 6: 输入 → Sink 路径
|
||
|
||
```
|
||
search (str) → server_repo.get_paginated() → Server.name.ilike(参数化) → MySQL
|
||
sort_by (str) → _SORT_COLUMNS.get(白名单) → Server.id(默认) → ORDER BY
|
||
sort_order (str) → asc/desc 分支 → ORDER BY 方向
|
||
is_online (bool) → WHERE Server.is_online == ? + Redis post-filter
|
||
```
|
||
|
||
---
|
||
|
||
## Step 7: 归类
|
||
|
||
| 类别 | 结论 |
|
||
|------|------|
|
||
| 安全 | 无新增风险,白名单+参数化+XSS转义全覆盖 |
|
||
| 逻辑 | over-fetch +10 补偿合理,post-filter 截断正确 |
|
||
| 性能 | DB 级分页替代全量加载,搜索走 LIKE 索引(如有) |
|
||
| 质量 | 修复 12 个预存 ruff 错误 + 1 个 F821 bug |
|
||
|
||
---
|
||
|
||
## Step 8: DoD
|
||
|
||
- [x] ruff check: All checks passed
|
||
- [x] bandit: No issues identified
|
||
- [x] import server.main: 成功
|
||
- [x] 无 TODO/FIXME
|
||
- [x] 无静默吞错(S110 全部改为 logger.warning/debug)
|
||
- [x] 审计日志: list_servers 是 GET 端点,无需审计(只读)
|
||
- [x] Playwright 浏览器验证: 搜索/排序/筛选/分页全部通过
|
||
- [x] 服务器部署成功 + 健康检查通过
|