# Phase 2a 逐行走读 — `server/api/*.py`

**标准**: `docs/project/line-walk-audit-standard.md`  
**日期**: 2026-05-22  
**范围**: `server/api/` 全部 16 个 Python 文件（3 批 × 5/5/6）  
**关联**: Phase 2b `docs/reports/audit-phase-2b-line-walk.md`

---

## 走读签字

| 项 | 值 |
|----|-----|
| 计划文件数 | 16 |
| 完成文件数 | 16 |
| 新增 FINDING | 18 |
| Phase 1 复核 | P0 WebSSH IDOR、P0 Agent exec 均已 VERIFY |

---

## 批次 A（安全入口，5 文件）

### 文件清单 — 批次 A

| 文件 | N | Read | H | Closure | 状态 |
|------|---|------|---|---------|------|
| `agent.py` | 274 | 1–274 | 8 | 8 | 逐行完成 ✓ |
| `webssh.py` | 329 | 1–329 | 6 | 6 | 逐行完成 ✓ |
| `auth_jwt.py` | 174 | 1–174 | 3 | 3 | 逐行完成 ✓ |
| `auth.py` | 237 | 1–237 | 2 | 2 | 逐行完成 ✓ |
| `install.py` | 652 | 1–652 | 9 | 9 | 逐行完成 ✓ |

### 入口表 — 批次 A

| 方法 | 路径 | 鉴权 |
|------|------|------|
| POST | `/api/agent/heartbeat` | X-API-Key（全局或 per-server） |
| POST | `/api/agent/exec` | X-API-Key（仅全局） |
| WS | `/ws/terminal/{server_id}` | JWT `?token=`（应绑定 server_id） |
| * | `/api/auth/login` 等 | 见 auth.py |
| * | `/api/install/*` | **无 JWT**（安装期设计；生产仍注册） |

### Findings — 批次 A（新增）

| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |
|----|---------|----|----|------|----------|
| F2a-01 | `webssh.py:94-97` | VULN | **P0** | 登录 JWT 无 `server_id` 时跳过绑定检查；`terminal.html` 用 `access_token` → **任意 server_id IDOR Shell** | 强制 JWT 含 `server_id` 且 `==` path；或签发 WebSSH 专用短期 token |
| F2a-02 | `agent.py:222-246` | VULN | **P0** | `/api/agent/exec` + `create_subprocess_shell`：持全局 API Key 即 **Nexus 本机 RCE** | 移出中心 API 或仅转发 Agent；禁止中心机 shell |
| F2a-03 | `agent.py:48,65,81,86` | RISK | P2 | API Key 用 `==`/`!=`，非 `secrets.compare_digest` | 常量时间比较 |
| F2a-04 | `agent.py:238` | VULN | P1 | `sudo {command}` 字符串拼接，无 shell 转义 | `shlex.quote(command)` 或 argv |
| F2a-05 | `webssh.py:76,83` | RISK | P2 | JWT 在 WebSocket URL query，易进 access/proxy 日志 | POST 换票或 Sec-WebSocket-Protocol |
| F2a-06 | `webssh.py:110-121` | BUG | P2 | 鉴权/凭据失败 `return` 未 `close`，客户端挂起 | `close(4003)` + reason |
| F2a-07 | `auth.py:151-165` | RISK | P2 | `totp/disable` 仅需 JWT，无 TOTP/密码二次确认 | 要求 `totp_code` 或当前密码 |
| F2a-08 | `install.py:386-517` | RISK | P1 | 生产环境 `/api/install/*` 仍注册；`init-db` 仅拦 `_is_installed()` | 安装完成后卸载路由或中间件 403 |
| F2a-09 | `install.py:590-599` | VULN | P2 | 更新 `.env` 品牌名未 `_escape_env_value`，可注入换行 | 复用 `_escape_env_value` |
| F2a-10 | `auth_jwt.py:26-35` | EXT | P2 | 无全局 JWT 中间件，仅靠各路由 `Depends`；漏挂即裸奔 | 全局 dependency 或路由组默认鉴权 |

### Closure — 批次 A（节选；len(H)=Closure）

**agent.py**

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| agent.py | 48 | PY-08 | FINDING | P2 | F2a-03 |
| agent.py | 65 | PY-08 | FINDING | P2 | 全局 exec 严格校验，仍非 digest |
| agent.py | 81,86 | PY-08 | FINDING | P2 | per-server / global 比较 |
| agent.py | 149-150 | PY-05 | SAFE | — | heartbeat 二次 `_verify_server_api_key` |
| agent.py | 222-246 | PY-02 | FINDING | P0 | F2a-02 |
| agent.py | 238 | PY-01 | FINDING | P1 | F2a-04 |
| agent.py | 195-196 | PY-09 | SAFE | — | Redis 告警跟踪失败 debug，不吞认证 |
| agent.py | 155-163 | PY-01 | SAFE | — | Redis key 为 int server_id |

**webssh.py**

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| webssh.py | 45-46 | PY-06 | SAFE | — | `settings.SECRET_KEY` 非字面量 |
| webssh.py | 72 | PY-05 | FINDING | P2 | WS 路由 |
| webssh.py | 94-97 | PY-05 | FINDING | P0 | F2a-01 |
| webssh.py | 152 | PY-03 | SAFE | — | ssh_pool.acquire，非用户拼命令 |
| webssh.py | 217 | PY-03 | SAFE | — | 危险命令仅告警 |
| webssh.py | 110-121 | PY-05 | FINDING | P2 | F2a-06 |

**auth_jwt.py**

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| auth_jwt.py | 26-35 | PY-05 | FINDING | P2 | F2a-10 文档化 public 前缀 |
| auth_jwt.py | 83-86 | PY-06 | SAFE | — | JWT decode 标准 |
| auth_jwt.py | 57-59 | PY-05 | SAFE | — | public 路径可无 token |

**auth.py**

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| auth.py | 56-77 | PY-05 | SAFE | — | login 公开 + AuthService 防暴破 |
| auth.py | 151-165 | PY-05 | FINDING | P2 | F2a-07 |
| auth.py | 112-148 | PY-05 | SAFE | — | TOTP setup/enable 校验 admin_id==self |

**install.py**

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| install.py | 182-188 | PY-01 | SAFE | — | `_escape_php_string` |
| install.py | 127-141 | PY-06 | SAFE | — | 密钥 `secrets.token_*` 生成 |
| install.py | 264-284 | PY-02 | SAFE | — | subprocess 固定 argv |
| install.py | 421,431 | PY-04 | SAFE | — | `text()` + 绑定参数 |
| install.py | 386-390 | PY-05 | FINDING | P1 | F2a-08 |
| install.py | 590-599 | PY-10 | FINDING | P2 | F2a-09 |

---

## 批次 B（业务 API，5 文件）

### 文件清单 — 批次 B

| 文件 | N | Read | H | Closure | 状态 |
|------|---|------|---|---------|------|
| `sync_v2.py` | 200 | 1–200 | 3 | 3 | 逐行完成 ✓ |
| `servers.py` | 435 | 1–435 | 4 | 4 | 逐行完成 ✓ |
| `scripts.py` | 263 | 1–263 | 4 | 4 | 逐行完成 ✓ |
| `dependencies.py` | 181 | 1–181 | 2 | 2 | 逐行完成 ✓ |
| `settings.py` | 404 | 1–404 | 3 | 3 | 逐行完成 ✓ |

### Findings — 批次 B

| ID | 文件:行 | 类 | 度 | 描述 |
|----|---------|----|----|------|
| F2a-11 | `sync_v2.py:58-72` | VULN | P1 | 认证用户可 POST 任意 `commands[]` → SSH（链至 2b F2b-04） |
| F2a-12 | `servers.py:305` | RISK | P1 | `generate_agent_api_key` 响应返回**完整** per-server key |
| F2a-13 | `servers.py:110-133` | BUG | P2 | `stats.online` 用 MySQL `is_online`，与列表 Redis 不一致 |
| F2a-14 | `scripts.py:37,167` | BUG | P2 | `GET /credentials` 注册在 `/{id}` **之后**，路径被 int 路由吃掉 → 422 |
| F2a-15 | `scripts.py:126-149` | VULN | P1 | JWT 用户任意命令 → Agent exec（`check_dangerous_command` 仅告警不拦） |
| F2a-16 | `settings.py:60-67` | RISK | P1 | 任意 JWT 管理员可调 `/api-key/reveal` 得全局 API_KEY |
| F2a-17 | `dependencies.py:167-180` | RISK | P2 | 危险命令检测**不阻断**执行 |

### Closure — 批次 B（摘要）

| 文件 | 行号 | 规则 | 判定 | 严重度 |
|------|------|------|------|--------|
| sync_v2.py | 39-55 | PY-05 | SAFE | — | JWT + engine |
| sync_v2.py | 58-72 | PY-05 | FINDING | P1 | F2a-11 |
| sync_v2.py | 156 | PY-03 | SAFE | — | `shlex.quote(path)` |
| servers.py | 185-188 | PY-06 | SAFE | — | encrypt_value 存凭据 |
| servers.py | 305 | PY-06 | FINDING | P1 | F2a-12 |
| servers.py | 396 | PY-06 | SAFE | — | 列表仅前缀脱敏 |
| servers.py | 110-114 | PY-09 | FINDING | P2 | F2a-13 |
| scripts.py | 37 vs 167 | PY-05 | FINDING | P2 | F2a-14 路由顺序 |
| scripts.py | 126-149 | PY-03 | FINDING | P1 | F2a-15 |
| scripts.py | 251-262 | PY-06 | SAFE | — | credential 不返密码 |
| dependencies.py | 108-143 | PY-09 | SAFE | — | D7 session 来自 middleware |
| dependencies.py | 167-180 | PY-07 | FINDING | P2 | F2a-17 |
| settings.py | 24,79-80 | PY-06 | SAFE | — | IMMUTABLE_KEYS |
| settings.py | 60-67 | PY-06 | FINDING | P1 | F2a-16 |

---

## 批次 C（其余 API，6 文件）

### 文件清单 — 批次 C

| 文件 | N | Read | H | Closure | 状态 |
|------|---|------|---|---------|------|
| `websocket.py` | 374 | 1–374 | 4 | 4 | 逐行完成 ✓ |
| `assets.py` | 354 | 1–354 | 1 | 1 | 逐行完成 ✓ |
| `search.py` | 120 | 1–120 | 2 | 2 | 逐行完成 ✓ |
| `health.py` | 47 | 1–47 | 2 | 2 | 逐行完成 ✓ |
| `schemas.py` | 225 | 1–225 | 2 | 2 | 逐行完成 ✓ |
| `__init__.py` | 1 | 1–1 | 0 | 0 | 规则零命中 ✓ |

### Findings — 批次 C

| ID | 文件:行 | 类 | 度 | 描述 |
|----|---------|----|----|------|
| F2a-18 | `websocket.py:213,76` | RISK | P2 | 告警 WS 同样 JWT 在 URL（与 webssh 同类） |

### Closure — 批次 C（摘要）

| 文件 | 行号 | 规则 | 判定 | 严重度 |
|------|------|------|------|--------|
| websocket.py | 210-228 | PY-05 | SAFE | — | JWT 校验 admin |
| websocket.py | 213 | PY-05 | FINDING | P2 | F2a-18 token in query |
| websocket.py | 259-260 | PY-06 | SAFE | — | SECRET_KEY 来自 settings |
| websocket.py | 334-339 | PY-09 | SAFE | — | Telegram 冷却，WS 仍广播 |
| assets.py | 全路由 | PY-05 | SAFE | — | 均 `Depends(get_current_admin)` |
| search.py | 22-28,43 | PY-04 | SAFE | — | LIKE 转义 |
| health.py | 15 | PY-05 | SAFE | — | 公开 health 设计 |
| health.py | 38 | PY-04 | SAFE | — | `SELECT 1` 常量 |
| schemas.py | 24-27 | PY-07 | SAFE | — | AgentExec 有 timeout 上限 |
| schemas.py | 99-103 | PY-07 | RISK | P2 | `commands: List[str]` 无内容校验，依赖下游 |

---

## 数据流（API 层 → sink）

```
浏览器 JWT ──► /api/scripts/exec ──► ScriptService ──► Agent http://host/exec
浏览器 JWT ──► /api/sync/commands ──► SyncEngineV2 ──► exec_ssh_command (SSH shell)
Agent X-API-Key ──► /api/agent/heartbeat ──► Redis + MySQL
Agent X-API-Key ──► /api/agent/exec ──► create_subprocess_shell (Nexus 本机)
浏览器 JWT ?token= ──► /ws/terminal/{id} ──► asyncssh shell (IDOR 若 token 无 server_id)
未认证 ──► /api/install/init-db ──► .env + config.php + DDL
```

---

## Phase 1 复核

| Phase 1 ID | 结论 | 行号 |
|------------|------|------|
| P0 WebSSH IDOR | **VERIFY** | `webssh.py:94-97` + `terminal.html:79-83` |
| P0 `/api/agent/exec` RCE | **VERIFY** | `agent.py:222-246` |
| P1 API Key reveal | **VERIFY** | `settings.py:60-67` |
| S-19/S-20 | 入口在 `sync_v2.py:75-88` → 2b 已记录 | — |

---

## 八类汇总（本目录）

| 类 | 条数 | 代表 |
|----|------|------|
| VULN | 7 | F2a-01,02,04,11,15 + install env |
| RISK | 8 | API Key 比较、install 暴露、reveal、JWT URL |
| BUG | 3 | webssh 静默失败、stats 不一致、credentials 路由 |
| EXT | 1 | 无全局 JWT 中间件 |
| PERF | 0 | — |
| 其他 | 0 | assets/search/health 无新增高危 |

---

## 下一批

**Phase 2c**: `server/infrastructure/**/*.py`（SSH 池、Redis、crypto、repos）
