# 审计报告 — 服务器列表迭代
## 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() 将 `"` 转义为 `&quot;`
- **判定**: ✅ 安全。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] 服务器部署成功 + 健康检查通过
