# 审计报告 — 服务器列表迭代 ## 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] 服务器部署成功 + 健康检查通过