# Phase 2b 逐行走读 — `server/application/services/*.py`

**标准**: `docs/project/line-walk-audit-standard.md`  
**日期**: 2026-05-22  
**范围**: 本目录 6 个 Python 文件（本批全部完成）  
**未含**: 「攻击面 Top N」式抽查；每条 `rg` 安全命中均已 Closure

---

## 走读签字

| 项 | 值 |
|----|-----|
| 计划文件数 | 6 |
| 完成文件数 | 6 |
| 新增 FINDING | 9 |
| Phase 1 复核 | S-19/S-20 已 VERIFY；S-18 在本批确认 |

---

## 文件清单

| 文件 | 行数 N | Read 范围 | rg 安全命中 H | Closure | 状态 |
|------|--------|-----------|---------------|---------|------|
| `__init__.py` | 8 | 1–8 | 0 | 0 | 逐行完成 ✓ |
| `auth_service.py` | 357 | 1–357 | 4 | 4 | 逐行完成 ✓ |
| `server_service.py` | 118 | 1–118 | 2 | 2 | 逐行完成 ✓ |
| `script_service.py` | 264 | 1–264 | 4 | 4 | 逐行完成 ✓ |
| `sync_service.py` | 268 | 1–268 | 4 | 4 | 逐行完成 ✓ |
| `sync_engine_v2.py` | 382 | 1–382 | 11 | 11 | 逐行完成 ✓ |

---

## Findings（仅 FINDING）

| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |
|----|---------|----|----|------|----------|
| F2b-01 | `sync_engine_v2.py:234` | VULN | P1 | `echo {key}={safe_value}` 仅 quote 值，含 `'` 可断 shell | `shlex.quote(f"{key}={value}")` 或不用 shell |
| F2b-02 | `sync_engine_v2.py:236` | VULN | P1 | `sysctl -w {key}={safe_value}` 同上 | 同上 |
| F2b-03 | `sync_engine_v2.py:284` | VULN | P1 | `cp {backup_file}`：`backup_file` 来自 `ls` 未 quote/校验 | 白名单路径 + `shlex.quote` |
| F2b-04 | `sync_engine_v2.py:158` | VULN | P1 | `sync_commands` 将 API 传入的 `cmd` 原样 `exec_ssh_command` | 危险命令阻断或 argv 执行 |
| F2b-05 | `sync_engine_v2.py:101` | BUG | P2 | `duration_seconds` 用 `.seconds` 非 `total_seconds()` | 改用 `total_seconds()` |
| F2b-06 | `sync_service.py:117` | BUG | P1 | `completed += 1` 在嵌套函数内无 `nonlocal`，成功时抛 `UnboundLocalError` | 添加 `nonlocal completed, failed` 或改返回值累加 |
| F2b-07 | `script_service.py:216` | RISK | P1 | Agent 调用 `http://` 明文，MITM 可改命令/响应 | 强制 HTTPS 或 mTLS |
| F2b-08 | `server_service.py:56` | RISK | P1 | 健康检查 `http://{domain}:{agent_port}` 明文 | 同上 |
| F2b-09 | `script_service.py:179` | VULN | P2 | `test_credential_connection` 返回含 `-p{password}` 的完整 shell 命令 | API 勿返回 `command` 字段，仅返回测试结果 |

---

## 数据流摘要（外部输入 → sink）

| 文件 | 输入来源 | 危险 sink |
|------|----------|-----------|
| `auth_service.py` | `login`/`refresh` 参数 | JWT 签发、DB 存 refresh |
| `server_service.py` | `server_ids`, Agent 响应 | `httpx` → Agent HTTP |
| `script_service.py` | `command`, `server_ids`, `credential` | `httpx` → Agent `/api/exec` |
| `sync_service.py` | `source_path`, `server` 记录 | `exec_ssh_command` → 远程 rsync |
| `sync_engine_v2.py` | `config_updates`, `commands`, `local_path`/`remote_path` | `exec_ssh_command` / SFTP |

入口表：本目录无 HTTP 路由（由 `server/api/*.py` 调用），入口表见 Phase 2a。

---

## Closure 全表

### `__init__.py`（H=0）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| — | — | — | — | — | 规则零命中；仅 import，Read 1–8 |

### `auth_service.py`

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| auth_service.py | 143 | PY-08 | SAFE | — | refresh token 精确匹配 + version 检测 |
| auth_service.py | 147-153 | PY-08 | SAFE | — | 遗留 refresh 仅兼容；审计已记录 |
| auth_service.py | 243 | PY-06 | SAFE | — | TOTP 开通需返回 secret/uri，非硬编码 |
| auth_service.py | 292,313 | PY-06 | SAFE | — | 使用 `settings.SECRET_KEY` 非字面量 |

### `server_service.py`

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| server_service.py | 56 | PY-01 | FINDING | P1 | 见 F2b-08，明文 HTTP |
| server_service.py | 61 | PY-01 | SAFE | — | `X-API-Key` 来自 DB 字段，非拼接命令 |

### `script_service.py`

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| script_service.py | 173-177 | PY-01 | SAFE | — | mysql 测试命令各段 `shlex.quote` |
| script_service.py | 179 | PY-01 | FINDING | P2 | 见 F2b-09，命令泄露到 API 响应 |
| script_service.py | 216 | PY-01 | FINDING | P1 | 见 F2b-07 |
| script_service.py | 224 | PY-03 | SAFE | — | JSON body 至 Agent，命令已在 Nexus 侧拼接 |

### `sync_service.py`

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| sync_service.py | 117 | PY-09 | FINDING | P1 | 见 F2b-06，`nonlocal` 缺失 |
| sync_service.py | 177-181 | PY-01 | SAFE | — | rsync 路径/主机 `shlex.quote` |
| sync_service.py | 179 | PY-01 | SAFE | — | `server.port` 为整数，非用户自由文本 |
| sync_service.py | 185 | PY-03 | SAFE | — | 命令已引用，经 asyncssh shell |

### `sync_engine_v2.py`

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| sync_engine_v2.py | 95-96 | PY-01 | SAFE | — | rsync 双路径 `shlex.quote` |
| sync_engine_v2.py | 101 | PY-09 | FINDING | P2 | 见 F2b-05 |
| sync_engine_v2.py | 158 | PY-03 | FINDING | P1 | 见 F2b-04，任意命令 SSH 执行 |
| sync_engine_v2.py | 232 | PY-01 | SAFE | — | sed 用 `re.escape(key)`，key 已白名单 |
| sync_engine_v2.py | 234 | PY-01 | FINDING | P1 | 见 F2b-01 |
| sync_engine_v2.py | 236 | PY-01 | FINDING | P1 | 见 F2b-02 |
| sync_engine_v2.py | 240-241 | PY-01 | SAFE | — | 路径为常量 `BACKUP_CONFIG_PATH` |
| sync_engine_v2.py | 275-276 | PY-03 | SAFE | — | find_cmd 路径常量 |
| sync_engine_v2.py | 284-285 | PY-01 | FINDING | P1 | 见 F2b-03 |
| sync_engine_v2.py | 335-338 | PY-10 | SAFE | — | SFTP 路径由 API 层 Pydantic 传入；需在 2a 验证路径规范 |

---

## 八类核对（本目录）

| 类 | 涉及文件 |
|----|----------|
| BUG | `sync_service.py`, `sync_engine_v2.py` |
| VULN | `sync_engine_v2.py`, `script_service.py` |
| RISK | `server_service.py`, `script_service.py` |
| PERF | — |
| EXT | — |
| STYLE | — |
| UX | — |
| OPT | — |

---

## Phase 1 项复核

| Phase 1 ID | 本批结论 |
|------------|----------|
| S-19 | **VERIFY** — `sync_engine_v2.py:234,236` |
| S-20 | **VERIFY** — `sync_engine_v2.py:284` |
| S-18 | **VERIFY** — `script_service.py:216`, `server_service.py:56` |

---

## 下一批建议

Phase **2a**: `server/api/*.py`（入口表 + JWT/Agent/install 路由）
