From 722917bfc911c9abc0fdb5ba182d2750a16680c6 Mon Sep 17 00:00:00 2001 From: Nexus Agent Date: Sat, 6 Jun 2026 23:57:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20API=20=E5=85=A8=E9=87=8F=E5=B7=A1?= =?UTF-8?q?=E6=A3=80=20B1=E2=80=93B6=20=E4=BF=AE=E5=A4=8D=20server=5Fids?= =?UTF-8?q?=20=E4=B8=8A=E9=99=90=E4=B8=8E=20pending=20=E5=AE=A1=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 限制健康检查/推送/脚本执行的 server_ids 规模,补齐 pending 重试失败与删除审计,并附分批巡检报告。 Co-authored-by: Cursor --- .../2026-06-06-api-audit-b1-servers-webssh.md | 26 ++++++ .../2026-06-06-api-audit-b2-sync-files.md | 17 ++++ .../changelog/2026-06-06-api-audit-closure.md | 21 +++++ .../2026-06-06-api-audit-b1-servers-webssh.md | 84 +++++++++++++++++++ .../2026-06-06-api-audit-b2-sync-files.md | 58 +++++++++++++ docs/reports/2026-06-06-api-audit-b3-auth.md | 18 ++++ ...026-06-06-api-audit-b4-scripts-terminal.md | 19 +++++ ...-06-api-audit-b5-settings-search-assets.md | 16 ++++ ...06-api-audit-b6-agent-install-ws-health.md | 16 ++++ docs/reports/2026-06-06-api-audit-closure.md | 43 ++++++++++ server/api/schemas.py | 8 +- server/api/servers.py | 15 ++++ tests/test_security_unit.py | 21 +++++ 13 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/2026-06-06-api-audit-b1-servers-webssh.md create mode 100644 docs/changelog/2026-06-06-api-audit-b2-sync-files.md create mode 100644 docs/changelog/2026-06-06-api-audit-closure.md create mode 100644 docs/reports/2026-06-06-api-audit-b1-servers-webssh.md create mode 100644 docs/reports/2026-06-06-api-audit-b2-sync-files.md create mode 100644 docs/reports/2026-06-06-api-audit-b3-auth.md create mode 100644 docs/reports/2026-06-06-api-audit-b4-scripts-terminal.md create mode 100644 docs/reports/2026-06-06-api-audit-b5-settings-search-assets.md create mode 100644 docs/reports/2026-06-06-api-audit-b6-agent-install-ws-health.md create mode 100644 docs/reports/2026-06-06-api-audit-closure.md diff --git a/docs/changelog/2026-06-06-api-audit-b1-servers-webssh.md b/docs/changelog/2026-06-06-api-audit-b1-servers-webssh.md new file mode 100644 index 00000000..c18e7eeb --- /dev/null +++ b/docs/changelog/2026-06-06-api-audit-b1-servers-webssh.md @@ -0,0 +1,26 @@ +# 2026-06-06 — API 巡检 B1(servers + webssh)修复 + +## 摘要 + +完成 servers/webssh 模块 8 维安全审计;修复健康检查 `server_ids` 无上限与 pending 重试/删除审计缺口。 + +## 动机 + +按计划 B1→B6 API 巡检,servers 大部 CRUD 此前未 BUG 通读;需消除可观测的资源耗尽与审计不一致。 + +## 涉及文件 + +- `server/api/schemas.py` — `ServerCheck.server_ids` max_length=50 +- `server/api/servers.py` — pending retry 失败审计、delete pending detail +- `tests/test_security_unit.py` — ServerCheck 上限回归 +- `docs/reports/2026-06-06-api-audit-b1-servers-webssh.md` + +## 迁移 / 重启 + +无;重启 API 即可。 + +## 验证 + +```bash +.venv/bin/pytest tests/test_security_unit.py tests/test_credential_poller.py -q +``` diff --git a/docs/changelog/2026-06-06-api-audit-b2-sync-files.md b/docs/changelog/2026-06-06-api-audit-b2-sync-files.md new file mode 100644 index 00000000..3f0c7abb --- /dev/null +++ b/docs/changelog/2026-06-06-api-audit-b2-sync-files.md @@ -0,0 +1,17 @@ +# 2026-06-06 — API 巡检 B2(sync + files)修复 + +## 摘要 + +审计 sync_v2 / files / 路径校验模块;为 `SyncFiles` 与 `SyncVerify` 的 `server_ids` 增加 2000 上限,防止超大列表资源耗尽。 + +## 涉及文件 + +- `server/api/schemas.py` +- `tests/test_security_unit.py` +- `docs/reports/2026-06-06-api-audit-b2-sync-files.md` + +## 验证 + +```bash +.venv/bin/pytest tests/test_security_unit.py -q +``` diff --git a/docs/changelog/2026-06-06-api-audit-closure.md b/docs/changelog/2026-06-06-api-audit-closure.md new file mode 100644 index 00000000..00c04e70 --- /dev/null +++ b/docs/changelog/2026-06-06-api-audit-closure.md @@ -0,0 +1,21 @@ +# 2026-06-06 — API 全量巡检 B1–B6 closure + +## 摘要 + +按 8 维清单完成 12 个 API 文件 ~122 端点巡检;修复 3 处 `server_ids` 无上限与 pending 审计缺口;0 P0/P1。 + +## 修复文件 + +- `server/api/schemas.py` — ServerCheck(50)、SyncFiles/SyncVerify/ScriptExecute(2000) +- `server/api/servers.py` — pending retry 失败审计、delete detail +- `tests/test_security_unit.py` — 回归用例 + +## 报告 + +`docs/reports/2026-06-06-api-audit-*.md` + [closure](docs/reports/2026-06-06-api-audit-closure.md) + +## 验证 + +```bash +.venv/bin/pytest tests/test_security_unit.py tests/test_auth_refresh_reuse.py tests/test_credential_poller.py -q +``` diff --git a/docs/reports/2026-06-06-api-audit-b1-servers-webssh.md b/docs/reports/2026-06-06-api-audit-b1-servers-webssh.md new file mode 100644 index 00000000..3f3e3bde --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-b1-servers-webssh.md @@ -0,0 +1,84 @@ +# API 巡检 B1 — servers.py + webssh.py + +**日期**: 2026-06-06 +**范围**: `server/api/servers.py`(28 端点)、`server/api/webssh.py`(1 WS) +**方法**: 8 维清单(认证/越权/注入/路径/凭据/输入/错误/审计) + +## 结论 + +| 级别 | 数量 | 说明 | +|------|------|------| +| P0 | 0 | — | +| P1 | 0 | — | +| P2 | 1 | 已修:`ServerCheck.server_ids` 无上限 → DoS | +| H | 3 | 已记录/接受,见下表 | + +**批次结论**: **PASS**(修复已合入工作区) + +--- + +## 端点覆盖 + +全部 29 个路由均要求 `get_current_admin`(REST)或 WebSSH 专用 JWT(`purpose=webssh` + `server_id` 绑定)。 + +| 模块 | 端点 | 认证 | 审计 CUD | 备注 | +|------|------|------|----------|------| +| servers | CRUD `/` `/{id}` | JWT | ✓ | `_server_to_dict` 不返明文密码/私钥 | +| servers | `/stats` `/categories` `/logs` | JWT | — | 只读;stats 有 online 封顶 | +| servers | `/import` | JWT | ✓ | 1MB/500 行;CSV 注入防护 `_sanitize_csv_cell` | +| servers | `/batch/*` | JWT | ✓ | `server_ids` max 50;SSH `shlex.quote` | +| servers | `/add-by-ip` `/pending/*` | JWT | ✓(已补 retry 失败) | 轮询失败写 pending + 审计 | +| servers | `/agent-key` `/agent-install-cmd` | JWT + 密码复核 | ✓ | 不回落全局 API_KEY | +| servers | `/install-agent` | JWT | ✓ | 服务端 SSH 可用全局 KEY 作 fallback(H) | +| servers | `/check` | JWT | — | **已修** max 50 server_ids | +| webssh | `/ws/terminal/{id}` | JWT query | connect/disconnect | server_id 绑定;命令审计 + 危险命令检测 | + +--- + +## FINDING 明细 + +### P2-01 — `POST /api/servers/check` 无 `server_ids` 上限(已修) + +- **风险**: 传入数千 ID 触发并行 SSH 探测,资源耗尽。 +- **修复**: `ServerCheck.server_ids` 增加 `max_length=50`(与 `BatchAgentAction` 一致)。 +- **文件**: `server/api/schemas.py` + +### H-01 — WebSSH 遗留 token 无 `tv` 字段时回退 `updated_at` 宽限 + +- **位置**: `webssh.py` `_verify_webssh_token` +- **影响**: 改密后 5 秒内旧 WebSSH token 仍可能有效(新签发 token 均含 `tv`)。 +- **处置**: 接受(单人运维模型);新 token 路径已安全。 + +### H-02 — `install-agent` 无 per-server key 时回落 `settings.API_KEY` + +- **位置**: `servers.py` `install_agent_remote` +- **影响**: 仅用于 SSH 下发安装命令,不向 API 响应暴露全局 KEY。 +- **处置**: 接受;与 Agent 安装运维模型一致。 + +### H-03 — `retry_pending` 失败与 `delete_pending` 审计字段不完整(已修) + +- **修复**: 失败重试写 `retry_pending_server_failed` 审计;删除前读取 pending 写入 `detail`。 +- **文件**: `server/api/servers.py` + +--- + +## 安全亮点(无需改动) + +- `_server_to_dict`: `password_set` / `ssh_key_private_set` / agent key 预览 +- `update_server`: `ALLOWED_FIELDS` 白名单,禁止 mass-assign `agent_api_key` +- `_sudo_wrap`: 临时 sudoers 白名单命令 + 事后清理 +- `setup-files-sudo`: 密码经 stdin 喂 `sudo -S`,不进 argv +- WebSSH: token `server_id` 与路径参数必须一致(防 IDOR) +- CSV import: 大小/行数限制 + formula injection 防护 + +--- + +## 验证 + +```bash +.venv/bin/pytest tests/test_credential_poller.py tests/test_security_unit.py -q +``` + +--- + +**下一批**: B2 — `sync_v2.py` + `files.py` + `remote_path_validation.py` diff --git a/docs/reports/2026-06-06-api-audit-b2-sync-files.md b/docs/reports/2026-06-06-api-audit-b2-sync-files.md new file mode 100644 index 00000000..cfef5d5c --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-b2-sync-files.md @@ -0,0 +1,58 @@ +# API 巡检 B2 — sync_v2.py + files.py + remote_path_validation.py + +**日期**: 2026-06-06 +**范围**: 22 个 `/api/sync/*` 端点 + `GET /api/files/browse` + 路径校验模块 + +## 结论 + +| 级别 | 数量 | 说明 | +|------|------|------| +| P0 | 0 | — | +| P1 | 0 | — | +| P2 | 1 | 已修:`SyncFiles`/`SyncVerify` `server_ids` 无上限 | +| H | 1 | `_audit_sync` 写失败仅 warning(不阻断业务) | + +**批次结论**: **PASS** + +--- + +## 覆盖摘要 + +| 检查项 | 结果 | +|--------|------| +| 全部 sync 端点 JWT | ✓ 22/22 `get_current_admin` | +| 远程路径 | ✓ `normalize_remote_abs_path` / `remote_join` | +| 本地 staging | ✓ `ensure_under_nexus_upload` 限制 `/tmp/nexus_upload_*` | +| Shell 拼接 | ✓ 普遍 `shlex.quote` | +| 上传限制 | ✓ 单文件 100MB;ZIP 500MB | +| 剪贴板 copy/move | ✓ `assert_clipboard_transfer_safe` 防 dest 在源子树内 | +| files GET browse | ✓ ETag + 路径规范化 + 审计 | + +--- + +## FINDING + +### P2-02 — `SyncFiles` / `SyncVerify` `server_ids` 无 max_length(已修) + +- **风险**: 超大列表导致海量协程调度与 DB 查询(verify 虽 Semaphore(5) 仍会创建 N 个 task)。 +- **修复**: `max_length=2000`(对齐平台子机规模上限)。 +- **文件**: `server/api/schemas.py` + +### H-04 — `_audit_sync` 异常时 `logger.warning` 不向上抛 + +- **位置**: `sync_v2.py` `_audit_sync` +- **处置**: 接受 — 审计失败不应阻断文件操作;已有 exc_info 日志。 + +--- + +## 安全亮点 + +- `file-ops` delete 使用 `rm -rf` 但路径经 `normalize_remote_abs_path` 校验 +- `upload` 文件名剥离 `/` `\` +- `local-file-ops` rename 禁止 `..` / `.` +- `write-file` 5MB 上限 + etag 乐观锁(409) +- `read-file` 1MB 默认上限 + +--- + +**下一批**: B3 — auth + auth_jwt + dependencies diff --git a/docs/reports/2026-06-06-api-audit-b3-auth.md b/docs/reports/2026-06-06-api-audit-b3-auth.md new file mode 100644 index 00000000..427b5882 --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-b3-auth.md @@ -0,0 +1,18 @@ +# API 巡检 B3 — auth.py + auth_jwt.py + dependencies.py + +**日期**: 2026-06-06 + +## 结论: **PASS**(0 P0/P1/P2,无代码修改) + +| 模块 | 端点/职责 | 结果 | +|------|-----------|------| +| `auth.py` | login/refresh/logout/TOTP/password/webssh-token/me | ✓ refresh HttpOnly cookie;改密/TOTP 需 JWT | +| `auth_jwt.py` | JwtAuthMiddleware + `get_current_admin` | ✓ PUBLIC_PREFIXES 白名单;`tv` 版本校验 | +| `dependencies.py` | DI + `check_dangerous_command` | ✓ 无 session 泄漏;危险命令 warning 日志 | + +## 要点 + +- 登录失败 → 429 锁定(`auth_service`) +- Refresh 重用检测 + `token_version` 递增(`test_auth_refresh_reuse.py` 覆盖) +- WebSSH token 独立 `purpose=webssh` + `server_id` 绑定 +- `check_dangerous_command` 为**警告不阻断**(单人运维接受,与 Scripts/WebSSH 一致) diff --git a/docs/reports/2026-06-06-api-audit-b4-scripts-terminal.md b/docs/reports/2026-06-06-api-audit-b4-scripts-terminal.md new file mode 100644 index 00000000..ce92cab8 --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-b4-scripts-terminal.md @@ -0,0 +1,19 @@ +# API 巡检 B4 — scripts.py + terminal.py + +**日期**: 2026-06-06 + +## 结论: **PASS**(P2×1 已修) + +### P2-03 — `ScriptExecute.server_ids` 无上限(已修) + +- **修复**: `max_length=2000`(`schemas.py`) +- 引擎内部分批 50/批、并发 10,与文档一致 + +### 其他 + +| 检查 | 结果 | +|------|------| +| JWT 全覆盖 | ✓ | +| `check_dangerous_command` on exec | ✓ 警告级 | +| 长任务回调 HTTPS 校验 | ✓ `master_callback_url` 单测 | +| terminal quick-commands CRUD | ✓ JWT + 审计 | diff --git a/docs/reports/2026-06-06-api-audit-b5-settings-search-assets.md b/docs/reports/2026-06-06-api-audit-b5-settings-search-assets.md new file mode 100644 index 00000000..45381e9e --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-b5-settings-search-assets.md @@ -0,0 +1,16 @@ +# API 巡检 B5 — settings.py + search.py + assets.py + +**日期**: 2026-06-06 + +## 结论: **PASS**(无代码修改) + +| 模块 | 安全要点 | 结果 | +|------|----------|------| +| `settings.py` | reveal API/preset/ssh-key/telegram 均需 `_verify_reauth` | ✓ | +| `settings.py` | 不可改 `SECRET_KEY`/`API_KEY`/`ENCRYPTION_KEY`/`DATABASE_URL` | ✓ 指南约束 | +| `search.py` | `_escape_like` 防 SQL LIKE 注入 | ✓ | +| `assets.py` | nodes/platforms/command-logs/ssh-sessions | ✓ 全 JWT | + +### H — bing-wallpapers 公开 GET + +- 已在 `PUBLIC_PREFIXES`;只读壁纸 URL,无敏感数据(与 2026-06-04 closure 一致) diff --git a/docs/reports/2026-06-06-api-audit-b6-agent-install-ws-health.md b/docs/reports/2026-06-06-api-audit-b6-agent-install-ws-health.md new file mode 100644 index 00000000..0d14f077 --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-b6-agent-install-ws-health.md @@ -0,0 +1,16 @@ +# API 巡检 B6 — agent.py + install.py + websocket.py + health.py + +**日期**: 2026-06-06 + +## 结论: **PASS**(无代码修改) + +| 模块 | 认证 | 结果 | +|------|------|------| +| `agent.py` | `X-API-Key`(全局或 per-server) | ✓ 心跳/script-callback 鉴权 | +| `install.py` | 仅安装模式;已安装 → 403 | ✓ 历史 F2a-08/09 已修 | +| `websocket.py` | `/ws/` 在 PUBLIC_PREFIXES;sync/alert 内二次校验 token/batch_id | ✓ | +| `health.py` | `/health` 公开 | ✓ 无敏感信息 | + +### 安装向导边界 + +- 正常模式所有 `/api/install/*` 返回 403(除 status 探测逻辑在 install 模块内处理) diff --git a/docs/reports/2026-06-06-api-audit-closure.md b/docs/reports/2026-06-06-api-audit-closure.md new file mode 100644 index 00000000..813c9fd7 --- /dev/null +++ b/docs/reports/2026-06-06-api-audit-closure.md @@ -0,0 +1,43 @@ +# API 全量巡检 — 总 Closure + +**日期**: 2026-06-06 +**计划**: B1→B6 按 8 维清单(认证/越权/注入/路径/凭据/输入/错误/审计) +**策略**: 审计 + 当场修复(用户确认) + +## 批次索引 + +| 批 | 模块 | 报告 | P0 | P1 | P2 已修 | +|----|------|------|----|----|---------| +| B1 | servers + webssh | [b1](2026-06-06-api-audit-b1-servers-webssh.md) | 0 | 0 | 1 | +| B2 | sync + files | [b2](2026-06-06-api-audit-b2-sync-files.md) | 0 | 0 | 1 | +| B3 | auth | [b3](2026-06-06-api-audit-b3-auth.md) | 0 | 0 | 0 | +| B4 | scripts + terminal | [b4](2026-06-06-api-audit-b4-scripts-terminal.md) | 0 | 0 | 1 | +| B5 | settings + search + assets | [b5](2026-06-06-api-audit-b5-settings-search-assets.md) | 0 | 0 | 0 | +| B6 | agent + install + ws + health | [b6](2026-06-06-api-audit-b6-agent-install-ws-health.md) | 0 | 0 | 0 | + +## 修复汇总 + +| ID | 问题 | 修复 | +|----|------|------| +| P2-01 | `ServerCheck.server_ids` 无上限 | `max_length=50` | +| P2-02 | `SyncFiles`/`SyncVerify.server_ids` 无上限 | `max_length=2000` | +| P2-03 | `ScriptExecute.server_ids` 无上限 | `max_length=2000` | +| H-03 | pending retry 失败/删除审计不完整 | 补 audit + detail | + +## 已接受 H(不阻断) + +- WebSSH 遗留 token 无 `tv` 宽限 5s +- `install-agent` 回落全局 `API_KEY`(仅 SSH 下发,不返前端) +- `_audit_sync` 写失败仅日志 +- `check_dangerous_command` 警告不阻断执行 + +## 验证 + +```bash +.venv/bin/pytest tests/test_security_unit.py tests/test_auth_refresh_reuse.py tests/test_credential_poller.py -q +``` + +## Changelog + +- `docs/changelog/2026-06-06-api-audit-b1-servers-webssh.md` +- `docs/changelog/2026-06-06-api-audit-b2-sync-files.md` diff --git a/server/api/schemas.py b/server/api/schemas.py index 5cb2c64c..cad81d05 100644 --- a/server/api/schemas.py +++ b/server/api/schemas.py @@ -104,7 +104,7 @@ class ServerUpdate(BaseModel): class ServerCheck(BaseModel): - server_ids: List[int] = Field(..., min_length=1) + server_ids: List[int] = Field(..., min_length=1, max_length=50) class ServerImportResult(BaseModel): @@ -148,7 +148,7 @@ class FileUpload(BaseModel): # ── Sync ── class SyncFiles(BaseModel): - server_ids: List[int] = Field(..., min_length=1) + server_ids: List[int] = Field(..., min_length=1, max_length=2000) source_path: str = Field(..., min_length=1) target_path: Optional[str] = None sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$") @@ -299,7 +299,7 @@ class ReconcileStaleLogs(BaseModel): class SyncVerify(BaseModel): """Post-push verification: compare local source with remote target via md5sum.""" - server_ids: List[int] = Field(..., min_length=1) + server_ids: List[int] = Field(..., min_length=1, max_length=2000) source_path: str = Field(..., min_length=1) target_path: Optional[str] = None max_files: int = Field(200, ge=1, le=500) @@ -467,7 +467,7 @@ class ScriptUpdate(BaseModel): class ScriptExecute(BaseModel): script_id: Optional[int] = None command: str = Field(..., min_length=1) - server_ids: List[int] = Field(..., min_length=1) + server_ids: List[int] = Field(..., min_length=1, max_length=2000) credential_id: Optional[int] = None timeout: int = Field(60, ge=1, le=600) long_task: bool = False diff --git a/server/api/servers.py b/server/api/servers.py index 1510f70e..6ec31465 100644 --- a/server/api/servers.py +++ b/server/api/servers.py @@ -1169,6 +1169,17 @@ async def retry_pending_server( pending.last_attempt_at = datetime.now(timezone.utc) await pending_repo.update(pending) + ip_address = request.client.host if request.client else "" + audit_repo = AuditLogRepositoryImpl(db) + await audit_repo.create(AuditLog( + admin_username=admin.username, + action="retry_pending_server_failed", + target_type="pending_server", + target_id=pending.id, + detail=f"凭据轮询重试失败 地址={pending.domain}:{pending.port or 22} 第{pending.attempts}次", + ip_address=ip_address, + )) + return { "success": False, "pending_id": pending.id, @@ -1187,6 +1198,9 @@ async def delete_pending_server( from server.infrastructure.database.pending_server_repo import PendingServerRepositoryImpl repo = PendingServerRepositoryImpl(db) + pending = await repo.get_by_id(pending_id) + if not pending: + raise HTTPException(404, "待连接服务器不存在") if not await repo.delete(pending_id): raise HTTPException(404, "待连接服务器不存在") @@ -1197,6 +1211,7 @@ async def delete_pending_server( action="delete_pending_server", target_type="pending_server", target_id=pending_id, + detail=f"删除待连接 地址={pending.domain}:{pending.port or 22}", ip_address=ip_address, )) diff --git a/tests/test_security_unit.py b/tests/test_security_unit.py index 07ee8875..1656c83b 100644 --- a/tests/test_security_unit.py +++ b/tests/test_security_unit.py @@ -21,6 +21,27 @@ def test_dangerous_command_safe_empty(): assert check_dangerous_command("echo hello") == [] +def test_server_check_server_ids_max_fifty(): + from pydantic import ValidationError + + from server.api.schemas import ServerCheck + + ServerCheck(server_ids=list(range(1, 51))) + with pytest.raises(ValidationError): + ServerCheck(server_ids=list(range(1, 52))) + + +def test_sync_files_server_ids_max_two_thousand(): + from pydantic import ValidationError + + from server.api.schemas import SyncFiles, SyncVerify + + SyncFiles(server_ids=list(range(1, 2001)), source_path="/tmp/nexus_upload_x/a") + SyncVerify(server_ids=[1], source_path="/tmp/nexus_upload_x/a") + with pytest.raises(ValidationError): + SyncFiles(server_ids=list(range(1, 2002)), source_path="/tmp/nexus_upload_x/a") + + def test_parse_server_ids_invalid_json(): assert _parse_server_ids_json("not-json") == []