93 lines
3.9 KiB
Markdown
93 lines
3.9 KiB
Markdown
|
|
# 2026-06-02 MEDIUM 级 Bug 修复(6 项)
|
|||
|
|
|
|||
|
|
## 日期
|
|||
|
|
2026-06-02
|
|||
|
|
|
|||
|
|
## 变更摘要
|
|||
|
|
修复系统扫描中发现的 6 个 MEDIUM 级 Bug,提升安全性、数据准确性和系统健壮性。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## M1: 登录锁定按 username+ip 计数,可换 IP 绕过
|
|||
|
|
|
|||
|
|
**文件**: `server/infrastructure/database/admin_repo.py`
|
|||
|
|
|
|||
|
|
**问题**: `count_recent_failures` 的 WHERE 条件同时过滤 `username AND ip_address`,攻击者只需更换 IP 即可无限尝试同一账号密码。
|
|||
|
|
|
|||
|
|
**修复**: 移除 `ip_address` 维度,改为仅按 `username` 计数失败次数。换 IP 不再能绕过锁定。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## M2: 重复 PushRetryJob 创建(同一服务器多条 pending)
|
|||
|
|
|
|||
|
|
**文件**: `server/application/services/sync_engine_v2.py`
|
|||
|
|
|
|||
|
|
**问题**: 每次 push 失败都无条件创建新的 `PushRetryJob`,若多次触发同一路径的推送且反复失败,会堆积大量重复 pending 任务。
|
|||
|
|
|
|||
|
|
**修复**: 创建前先查询是否已存在相同 `(server_id, source_path, target_path, status=pending)` 的记录,存在则跳过创建。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## M3: schedule_runner 先标记已执行再同步,失败时调度记录丢失
|
|||
|
|
|
|||
|
|
**文件**: `server/background/schedule_runner.py`
|
|||
|
|
|
|||
|
|
**问题**: 执行前先将 `last_run_at = now` 并 `commit()`,若同步任务抛出异常,`last_run_at` 已更新,该次调度不会重新触发,相当于"静默跳过"了一次调度。
|
|||
|
|
|
|||
|
|
**修复**: 保存 `prev_last_run_at`,执行失败时将 `last_run_at` 回滚,允许下一个周期重新触发。`once` 模式不回滚(已标记 `enabled=False`,防止重复执行)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## M4: servers.py is_online 过滤后 total 仍用 DB 总数,分页不准
|
|||
|
|
|
|||
|
|
**文件**: `server/api/servers.py`
|
|||
|
|
|
|||
|
|
**问题**: 当传入 `is_online` 过滤参数时,Redis 覆盖后的内存过滤结果与 DB 总数不一致,`total` 和 `pages` 字段仍反映 DB 全量数量,前端分页逻辑错乱。
|
|||
|
|
|
|||
|
|
**修复**: 内存过滤后重新计算 `total = len(result)`,`pages` 同步更新。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## M5: asyncssh_pool exec_ssh_command 超时后未踢出坏连接
|
|||
|
|
|
|||
|
|
**文件**: `server/infrastructure/ssh/asyncssh_pool.py`
|
|||
|
|
|
|||
|
|
**问题**: `asyncio.TimeoutError` 在 `finally` 中仍会 `release()` 超时的连接,将其放回连接池,下一次 `acquire()` 可能复用这个状态未知的坏连接,导致命令执行结果不可靠。
|
|||
|
|
|
|||
|
|
**修复**: 超时和连接错误时改为 `close_connection()` 而非 `release()`,将坏连接从池中彻底移除;一般异常仍 `release()`(可能仍可用)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## M6: token_version += 1 在 NULL 时 TypeError
|
|||
|
|
|
|||
|
|
**文件**: `server/api/auth.py`, `server/application/services/auth_service.py`(3 处)
|
|||
|
|
|
|||
|
|
**问题**: DB 中 `token_version` 为 `NULL`(直接 INSERT 未设默认值时)时,`admin.token_version += 1` 抛 `TypeError: unsupported operand type(s) for +=: 'NoneType' and 'int'`,导致修改密码 / 禁用 TOTP / 强制登出接口 500。
|
|||
|
|
|
|||
|
|
**修复**: 所有 `token_version += 1` 统一改为 `(token_version or 0) + 1`,共修复 3 处。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 涉及文件
|
|||
|
|
|
|||
|
|
| 文件 | 修复项 |
|
|||
|
|
|------|-------|
|
|||
|
|
| `server/infrastructure/database/admin_repo.py` | M1(锁定维度) |
|
|||
|
|
| `server/application/services/sync_engine_v2.py` | M2(重复 RetryJob) |
|
|||
|
|
| `server/background/schedule_runner.py` | M3(调度回滚) |
|
|||
|
|
| `server/api/servers.py` | M4(分页 total) |
|
|||
|
|
| `server/infrastructure/ssh/asyncssh_pool.py` | M5(超时踢连接) |
|
|||
|
|
| `server/api/auth.py` | M6(token_version None) |
|
|||
|
|
| `server/application/services/auth_service.py` | M6(token_version None,2 处) |
|
|||
|
|
|
|||
|
|
## 需要迁移/重启
|
|||
|
|
- 后端重启生效(`supervisorctl restart nexus`)
|
|||
|
|
- 无数据库 schema 变更
|
|||
|
|
|
|||
|
|
## 验证方式
|
|||
|
|
1. `python -c "import server.main"` — 导入无报错 ✓
|
|||
|
|
2. 同一 IP 登录失败 5 次后,换 IP 仍被锁定
|
|||
|
|
3. 查看 `push_retry_jobs` 表,相同路径不再出现多条 pending
|
|||
|
|
4. 调度执行失败后 `last_run_at` 应保持原值
|
|||
|
|
5. 服务器列表按在线状态过滤,分页 total 与实际条目数一致
|