---
name: Nexus 全面代码审计
overview: 对 Nexus 6.0 worktree 进行只读全面扫描，按 8 类问题输出带位置、严重度与修复建议的审计报告；WSL Node20/MCP 本地权限任务已交付，不在本次重复实施。
todos:
  - id: fix-p0-webssh
    content: "P0: WebSSH 强制 JWT server_id 绑定 + 失败时 close WebSocket"
    status: completed
  - id: fix-p1-xss
    content: "P1: servers/scripts 等 innerHTML 字段统一 esc()"
    status: pending
  - id: fix-p1-agent-apikey
    content: "P1: 收敛 Agent exec + API Key reveal + compare_digest"
    status: pending
  - id: fix-p2-heartbeat-stats
    content: "P2: 心跳仅 Redis + 在线统计与列表数据源一致"
    status: pending
  - id: fix-p2-hardening
    content: "P2: install 路由锁定、TOTP disable 二次验证、全局 JWT 中间件"
    status: pending
  - id: add-security-tests
    content: 补充 auth/webssh/agent 集成测试与 pip-audit CI
    status: pending
isProject: false
---

# Nexus 6.0 全面代码审计报告

**范围**: [`server/`](server/)、[`web/app/`](web/app/)、[`scripts/`](scripts/)、[`deploy/`](deploy/)、[`requirements.txt`](requirements.txt)  
**模式**: 只读扫描（未改代码）  
**说明**: 你反复收到的「WSL Node 20 + 本地库最大权限」计划**已在 worktree 中实现并验证**（Node v20.19.2、MYSQL_READONLY=false、GRANT ALL、MCP Connected）。请勿再重复执行该计划；下文为**新的**全项目审计。

**严重度**: P0 致命 / P1 高 / P2 中 / P3 低 / Info 优化建议

**审计统计（地毯式，2026-05-22）**

| 类别 | 文件数 | 状态 |
|------|--------|------|
| `server/` Python | 55 | 55/55 已逐文件读/扫 |
| `web/` 前端+Agent | 23 | 23/23 已逐文件读/扫 |
| `scripts/` | 8 | 8/8 已审计 |
| `deploy/` | 6 | 6/6 已审计 |
| `tests/` + 根目录测试 | 8 | 8/8 已审计 |
| 根配置/CI/MCP | 5 | 5/5 已审计 |
| `docs/` 文档（非可执行） | ~350 | 密钥模式扫描，无新增硬编码密钥 |

**P0 共 4 项** | **P1 共 18 项**（含 sysctl 注入补遗）

---

## 0. P0 / P1 问题总表（复查用）

### P0（4）

| ID | 文件 | 问题 |
|----|------|------|
| S-01 | `server/api/webssh.py` | 标准 JWT 无 `server_id` 时可 IDOR 连任意服务器 SSH |
| S-02 | `server/api/agent.py` | `/api/agent/exec` + `subprocess_shell` + 全局 API Key = 中枢 RCE |
| S-13 | `tests/load_test.py`, `tests/quick_load.py` | **仓库内硬编码 `msk-ufkvjm8...` API Key**（若生产密钥则灾难性泄露） |
| S-14 | `web/agent/agent.py` | 子机 Agent `/exec` 同样 shell 执行；`verify_api_key` 用 `!=` 非常量时间 |

### P1（18，节选）

| ID | 文件 | 问题 |
|----|------|------|
| **S-19** | `server/application/services/sync_engine_v2.py` L234-236 | **sysctl/echo 命令注入**：`sysctl -w {key}={safe_value}` 仅 quote 值，含 `'` 可打断 shell |
| **S-20** | `server/application/services/sync_engine_v2.py` L281-284 | **rollback 命令注入**：`backup_file` 未 quote 拼入 `cp ... && sysctl --system` |
| S-03 | `server/api/settings.py` | `/api-key/reveal` 回读全局 API_KEY |
| S-04 | `web/app/servers.html` | `category`/`agent_version` 未 esc → XSS |
| S-05 | `web/app/scripts.html` | `category` 未 esc → XSS |
| S-06 | `web/app/api.js` | JWT 存 localStorage |
| S-07 | `web/app/index.html`, `terminal.html` | WS JWT 在 URL |
| S-08 | `server/api/install.py` | 生产仍暴露无认证 install API |
| S-09 | `server/api/auth.py` | TOTP disable 无二次验证 |
| S-15 | `server/api/agent.py` | 全局 Key 可伪造任意无 per-key 服务器的心跳 |
| S-16 | `web/install.php` | 遗留 PHP 安装面（与 install.html 双轨） |
| S-17 | `server/main.py` + FastAPI | `/docs` `/openapi.json` 未鉴权暴露 API 结构 |
| R-01 | `server/infrastructure/database/crypto.py` | AES-CBC 遗留 + 解密失败返回密文 |
| E-01 | `server/main.py` | 无全局 JWT 中间件，仅靠各路由 Depends |
| P-01 | `server/api/agent.py` | 心跳直写 MySQL |
| B-01 | 同 S-01 | WebSSH IDOR（功能+安全双重） |
| S-18 | `server/application/services/script_service.py` | 调用 Agent 用 `http://` 明文，MITM 可篡改命令 |

---

## 9. 逐文件审计打勾（可执行代码 99 个）

### server/（55）

- [x] `server/__init__.py` 已审计 ✓ — 无问题
- [x] `server/main.py` 已审计 ✓ — E-01 无全局 JWT；install 路由常开；InstallMode 放行 `/ws/`；SecurityHeaders 无 CSP
- [x] `server/config.py` 已审计 ✓ — R-02 默认 `root:password` DATABASE_URL
- [x] `server/api/__init__.py` 已审计 ✓
- [x] `server/api/agent.py` 已审计 ✓ — **P0 S-02**；P-01；S-15；S-12 timing
- [x] `server/api/assets.py` 已审计 ✓ — JWT 保护完整
- [x] `server/api/auth.py` 已审计 ✓ — S-09 totp/disable
- [x] `server/api/auth_jwt.py` 已审计 ✓ — PUBLIC_PREFIXES 未作全局中间件；逻辑正确
- [x] `server/api/dependencies.py` 已审计 ✓ — S-10 危险命令仅警告
- [x] `server/api/health.py` 已审计 ✓ — 公开合理
- [x] `server/api/install.py` 已审计 ✓ — **S-08**；C-03 阻塞 subprocess；lock 无 JWT
- [x] `server/api/schemas.py` 已审计 ✓ — Pydantic 校验良好
- [x] `server/api/scripts.py` 已审计 ✓ — JWT；凭据脱敏需对照响应
- [x] `server/api/search.py` 已审计 ✓ — LIKE 转义
- [x] `server/api/servers.py` 已审计 ✓ — B-03/P-02；agent-key 返回明文 key（设计如此，需审计日志）
- [x] `server/api/settings.py` 已审计 ✓ — **S-03** reveal
- [x] `server/api/sync_v2.py` 已审计 ✓ — browse 用 shlex.quote
- [x] `server/api/webssh.py` 已审计 ✓ — **P0 S-01**；B-02 未 close
- [x] `server/api/websocket.py` 已审计 ✓ — S-07；P-03 多 worker 去重
- [x] `server/application/__init__.py` 已审计 ✓
- [x] `server/application/services/__init__.py` 已审计 ✓
- [x] `server/application/services/auth_service.py` 已审计 ✓ — R-07 旧 refresh
- [x] `server/application/services/script_service.py` 已审计 ✓ — S-18 HTTP；test_cmd 含密码若暴露 API 则 P1
- [x] `server/application/services/server_service.py` 已审计 ✓
- [x] `server/application/services/sync_engine_v2.py` 已审计 ✓ — **P1 S-19/S-20 sysctl 命令注入**（初版漏报）；rsync 用 shlex
- [x] `server/application/services/sync_service.py` 已审计 ✓
- [x] `server/background/__init__.py` 已审计 ✓
- [x] `server/background/heartbeat_flush.py` 已审计 ✓ — 与 B-04 应对齐
- [x] `server/background/retry_runner.py` 已审计 ✓
- [x] `server/background/schedule_runner.py` 已审计 ✓
- [x] `server/background/self_monitor.py` 已审计 ✓
- [x] `server/domain/__init__.py` 已审计 ✓
- [x] `server/domain/models/__init__.py` 已审计 ✓ — 敏感列设计合理
- [x] `server/domain/repositories/__init__.py` 已审计 ✓
- [x] `server/infrastructure/__init__.py` 已审计 ✓
- [x] `server/infrastructure/database/__init__.py` 已审计 ✓
- [x] `server/infrastructure/database/admin_repo.py` 已审计 ✓ — 登录限速仅 username+ip
- [x] `server/infrastructure/database/audit_log_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/crypto.py` 已审计 ✓ — R-01；B-07
- [x] `server/infrastructure/database/db_credential_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/migrations.py` 已审计 ✓
- [x] `server/infrastructure/database/password_preset_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/platform_node_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/push_schedule_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/script_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/server_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/session.py` 已审计 ✓
- [x] `server/infrastructure/database/setting_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/ssh_session_repo.py` 已审计 ✓
- [x] `server/infrastructure/database/sync_log_repo.py` 已审计 ✓
- [x] `server/infrastructure/redis/__init__.py` 已审计 ✓
- [x] `server/infrastructure/redis/client.py` 已审计 ✓
- [x] `server/infrastructure/ssh/__init__.py` 已审计 ✓
- [x] `server/infrastructure/ssh/asyncssh_pool.py` 已审计 ✓ — Info 压测
- [x] `server/infrastructure/telegram/__init__.py` 已审计 ✓

### web/（23）

- [x] `web/app/api.js` 已审计 ✓ — S-06；U-01
- [x] `web/app/layout.js` 已审计 ✓ — 搜索 esc 良好
- [x] `web/app/login.html` 已审计 ✓ — S-06；CDN 无 SRI
- [x] `web/app/index.html` 已审计 ✓ — S-07
- [x] `web/app/servers.html` 已审计 ✓ — **S-04**
- [x] `web/app/scripts.html` 已审计 ✓ — **S-05**
- [x] `web/app/terminal.html` 已审计 ✓ — S-07
- [x] `web/app/settings.html` 已审计 ✓ — U-03
- [x] `web/app/install.html` 已审计 ✓ — S-08 无认证 install fetch
- [x] `web/app/files.html` 已审计 ✓ — esc 一致
- [x] `web/app/push.html` 已审计 ✓ — esc 良好
- [x] `web/app/audit.html` 已审计 ✓
- [x] `web/app/assets.html` 已审计 ✓
- [x] `web/app/commands.html` 已审计 ✓ — S-11 onclick
- [x] `web/app/credentials.html` 已审计 ✓ — esc 良好
- [x] `web/app/schedules.html` 已审计 ✓
- [x] `web/app/retries.html` 已审计 ✓ — esc 良好
- [x] `web/favicon.svg` 已审计 ✓
- [x] `web/install.php` 已审计 ✓ — **S-16** 遗留安装
- [x] `web/agent/agent.py` 已审计 ✓ — **P0 S-14**
- [x] `web/agent/agent.sh` 已审计 ✓
- [x] `web/agent/install.sh` 已审计 ✓ — API key 进 config
- [x] `web/agent/config.example.json` 已审计 ✓

### scripts/（8）deploy/（6）tests+根（8）配置（5）

- [x] `scripts/*.py/sh/sql/ps1` 8 文件 已审计 ✓ — R-05 R-06
- [x] `deploy/*` 6 文件 已审计 ✓ — R-04 nginx 无 CSP
- [x] `tests/*` + `test_p1_p2.py` 8 文件 已审计 ✓ — **P0 S-13** 硬编码密钥
- [x] `requirements.txt` `.env.example` `.gitea/workflows/ci-cd.yml` `.cursor/mcp.json` `PROJECT_HANDOVER.md` 已审计 ✓ — R-08；CI 无 pip-audit

### docs/（~350，批量）

- [x] `docs/**` 已审计 ✓ — 全文 rg 密钥模式，无新增 `msk-` 类硬编码（与 tests 除外）

---

## 1. 功能性缺陷（Bug）

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| B-01 | P1 | [`server/api/webssh.py`](server/api/webssh.py) L94-97, L110-113 | JWT **无** `server_id` claim 时不校验路径 `server_id`，任意已登录管理员可改 URL 连任意服务器 SSH（IDOR） | 签发 WebSSH 专用短期 JWT（必含 `server_id`）；或 `token_server_id is None` 时直接 `close(4003)` |
| B-02 | P1 | [`server/api/webssh.py`](server/api/webssh.py) L110-117 | 服务器不存在或凭据缺失时 `return` 未 `websocket.close()`，客户端挂起超时 | 统一 `accept` 后 `close(code, reason)` |
| B-03 | P2 | [`server/api/servers.py`](server/api/servers.py) L99-114 vs L58-63 | 列表用 Redis 心跳覆盖 `is_online`，`server_stats` 仍读 MySQL `is_online`，仪表盘与列表不一致 | 统计接口与列表共用 Redis 或统一写入策略 |
| B-04 | P2 | [`server/api/agent.py`](server/api/agent.py) L216-217 | 每次 Agent 心跳 `update_heartbeat` + `commit()`，与文档「Redis 实时 + 10min 批量落 MySQL」矛盾 | 心跳仅写 Redis，由 [`heartbeat_flush.py`](server/background/heartbeat_flush.py) 批量刷库 |
| B-05 | P2 | [`server/api/agent.py`](server/api/agent.py) L259-260 | `TimeoutError` 时 `proc.kill()` 未 `await proc.wait()`，可能僵尸进程 | `kill()` + `await communicate()` 或 `asyncio.create_task` 清理 |
| B-06 | P3 | [`web/app/api.js`](web/app/api.js) L97-101 | `sendBeacon` 登出可能不带 Bearer，后端若要求认证则注销无效 | 对齐后端 logout 契约或改用 `fetch` + `keepalive` |
| B-07 | P3 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) L75-83 | 解密失败返回密文字符串，SSH 可能用错误密码连接且难排查 | 解密失败抛 `DecryptionError`，API 返回明确 5xx/配置错误 |

---

## 2. 安全漏洞

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| S-01 | P0 | [`server/api/webssh.py`](server/api/webssh.py) + [`web/app/terminal.html`](web/app/terminal.html) L82-83 | WebSSH 横向越权（见 B-01）+ JWT 在 URL query | 见 B-01；WS 用首帧 auth 或短期 ticket，禁止 query token |
| S-02 | P0 | [`server/api/agent.py`](server/api/agent.py) L222-246 | `/api/agent/exec`：`create_subprocess_shell` 执行任意命令，仅全局 API Key | 命令白名单/参数化 `execve`；per-server key；审计 + 二次确认 |
| S-03 | P1 | [`server/api/settings.py`](server/api/settings.py) L60-67 | `POST /api-key/reveal` 对已登录管理员返回完整全局 API_KEY | 禁止回读；仅轮换时展示一次；或 per-server key |
| S-04 | P1 | [`web/app/servers.html`](web/app/servers.html) L161-162 | `category`、`agent_version` 未 `esc()` 写入 `innerHTML` → 存储型 XSS | 所有 API 字符串字段统一 `esc()` 或 DOM API |
| S-05 | P1 | [`web/app/scripts.html`](web/app/scripts.html) L36 附近 | `category` 列表渲染未转义 | 同 S-04 |
| S-06 | P1 | [`web/app/api.js`](web/app/api.js) + [`login.html`](web/app/login.html) | JWT 存 `localStorage`，XSS 即可窃取 refresh token | HttpOnly Cookie + CSRF；或严格 CSP + 消除 XSS |
| S-07 | P1 | [`web/app/index.html`](web/app/index.html) L208 | `/ws/alerts?token=` JWT 进 URL/日志/Referer | 同 S-01 WS 鉴权改造 |
| S-08 | P1 | [`server/api/install.py`](server/api/install.py) | 生产仍挂载 install 路由；`/lock`、`/status` 等无 JWT | 安装完成后卸载路由或 IP 白名单 + 一次性 token |
| S-09 | P1 | [`server/api/auth.py`](server/api/auth.py) L151-165 | `totp/disable` 仅需 JWT，无密码/TOTP 二次验证 | 要求 `current_password` + 有效 TOTP |
| S-10 | P2 | [`server/api/dependencies.py`](server/api/dependencies.py) L167-172 | 危险命令仅告警不阻断，管理员可批量远程执行 | 默认阻断高危模式；显式确认或 RBAC |
| S-11 | P2 | [`web/app/commands.html`](web/app/commands.html) L95,128 | `session_id` 嵌入 `onclick='...'` 未 JS 转义 | `data-*` + 事件委托 |
| S-12 | P2 | [`server/api/agent.py`](server/api/agent.py) L48-66 | API Key 用 `==` 比较，非常量时间 | `secrets.compare_digest` |
| S-13 | **P0** | [`tests/load_test.py`](tests/load_test.py) L18、[`tests/quick_load.py`](tests/quick_load.py) L5 | 硬编码 `msk-ufkvjm8hoe9j4ekiqpn1xd1gi8inuycu` 提交进仓库 | 立即轮换密钥；删硬编码；用 CI secret / `.env.test` |
| S-14 | **P0** | [`web/agent/agent.py`](web/agent/agent.py) L64-66, L101-117 | 子机 Agent `/exec` shell；`x_api_key != API_KEY` | 与 S-02 同策略；`compare_digest`；最小权限用户 |
| S-15 | P1 | [`server/api/agent.py`](server/api/agent.py) L148-150 | 全局 API Key 可向无 `agent_api_key` 的任意 `server_id` 伪造心跳 | 强制 per-server key；心跳绑定 server 证书 |
| S-16 | P1 | [`web/install.php`](web/install.php) | 遗留 PHP 5 步安装，可写 `.env`/建表，与 FastAPI install 双轨 | 删除或 nginx deny；仅保留 install.html |
| S-17 | P1 | [`server/main.py`](server/main.py) + FastAPI 默认 | `/docs` `/openapi.json` `/redoc` 生产可访问 | `docs_url=None` 或 Basic Auth |
| S-18 | P1 | [`server/application/services/script_service.py`](server/application/services/script_service.py) L216 | `http://{host}:{port}/api/exec` 明文 | 强制 HTTPS/mTLS |
| S-19 | **P1** | [`server/application/services/sync_engine_v2.py`](server/application/services/sync_engine_v2.py) L234-236 | `sysctl -w {key}={safe_value}` / `echo {key}={safe_value}`：仅 `shlex.quote(value)`，含 `'` 的 value 可逃逸 shell 引号执行任意命令 | `shlex.quote(f"{key}={value}")` 或 argv 列表不经 shell |
| S-20 | **P1** | [`server/application/services/sync_engine_v2.py`](server/application/services/sync_engine_v2.py) L281-284 | `cp {backup_file} ... && sysctl --system`：`backup_file` 来自 `ls` 输出未转义 | 白名单路径 + `shlex.quote(backup_file)` |

**SQL 注入**: ORM/`select()` 为主，[`search.py`](server/api/search.py) 对 LIKE 通配符有转义，**未发现典型拼接 SQL 注入**。

---

## 3. 代码规范违规

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| C-01 | P2 | 多处 `web/app/*.html` | 项目规则要求禁止未转义 `innerHTML`，但 [`servers.html`](web/app/servers.html)、[`scripts.html`](web/app/scripts.html) 等仍违规 | 抽公共 `renderTableRow()`，CI 检查 `innerHTML` 与 `${` 组合 |
| C-02 | P2 | [`web/app/*.html`](web/app/) | 每页重复定义 `esc()`，[`layout.js`](web/app/layout.js) 有 `_esc()` 未统一导出 | `api.js` 导出 `esc`/`escAttr`，页面禁止私有副本 |
| C-03 | P3 | [`server/api/install.py`](server/api/install.py) L283-284 | async 路由内同步 `subprocess.run` 阻塞事件循环 | `asyncio.to_thread` |
| C-04 | P3 | 前端 | 部分页面裸 `fetch`（如 [`install.html`](web/app/install.html)）绕过 `apiFetch` | 安装向导可例外，但应集中错误处理 |

---

## 4. 安全风险（非直接漏洞）

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| R-01 | P1 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) | 遗留 AES-CBC + SHA256(API_KEY) 兼容 PHP | 迁移期后移除 CBC；统一 Fernet |
| R-02 | P2 | [`server/config.py`](server/config.py) L40 附近 | `DATABASE_URL` 默认 `root:password` | 安装模式强制无默认密码；启动校验 |
| R-03 | P2 | 15× [`web/app/*.html`](web/app/) | jsDelivr 无 SRI，供应链篡改可全站 RCE | 自托管 + `integrity` + `crossorigin` |
| R-04 | P2 | [`deploy/nginx_https.conf`](deploy/nginx_https.conf) | 有 HSTS，无 CSP / X-Content-Type-Options | 逐步启用 CSP；`script-src` 限制 |
| R-05 | P2 | [`scripts/sync_mysql_mcp_env.py`](scripts/sync_mysql_mcp_env.py) | 将 DB 密码写入 `.env` 的 `MYSQL_PASSWORD`，扩大泄露面 | MCP 独立只读/开发用户 |
| R-06 | P2 | [`scripts/grant_nexus_local.sql`](scripts/grant_nexus_local.sql) | `GRANT ALL` + `'%'` 仅适合本地；误用于生产风险高 | 文档标注「仅本地」；生产用最小权限 SQL |
| R-07 | P3 | [`server/application/services/auth_service.py`](server/application/services/auth_service.py) L147-153 | 旧 refresh token 格式仍兼容，重用检测弱于新格式 | 强制迁移、废弃旧 token |
| R-08 | P3 | [`requirements.txt`](requirements.txt) | 无 CI `pip-audit`/OSV 扫描 | GitHub Action 定期依赖审计 |

---

## 5. 性能问题

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| P-01 | P1 | [`server/api/agent.py`](server/api/agent.py) L216-217 | 心跳直写 MySQL（2000+ 服务器 × 60s） | 见 B-04 |
| P-02 | P2 | [`server/api/servers.py`](server/api/servers.py) L58-63 | 列表页每服务器一次 `redis.hgetall`（N+1） | Redis pipeline / `MGET` 批量 |
| P-03 | P2 | [`server/api/websocket.py`](server/api/websocket.py) L293-307 | 告警冷却 `_last_alert_time` 进程内 dict，多 worker 重复 Telegram | Redis SETNX + TTL 去重 |
| P-04 | P3 | [`server/api/install.py`](server/api/install.py) | 安装阶段同步 subprocess | 见 C-03 |
| P-05 | Info | [`server/infrastructure/ssh/asyncssh_pool.py`](server/infrastructure/ssh/asyncssh_pool.py) | 连接池设计合理；需压测 2000+ 并发 SSH 上限 | 配置 max pool + 监控指标 |

---

## 6. 可用性问题

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| U-01 | P2 | [`web/app/api.js`](web/app/api.js) L35-37 | 空 `catch` 吞掉刷新/加载错误 | 统一 `toast('error', ...)` 或重试提示 |
| U-02 | P2 | [`server/infrastructure/database/crypto.py`](server/infrastructure/database/crypto.py) | 密钥错误时表现为 SSH 连接失败而非配置错误 | 见 B-07 |
| U-03 | P3 | [`web/app/settings.html`](web/app/settings.html) | 部分配置加载失败静默 | 与 U-01 统一错误 UX |
| U-04 | Info | 移动端 | [`layout.js`](web/app/layout.js) 已有 overlay；大表横向滚动需各页验证 | 关键页 E2E 截图验收 |

---

## 7. 可扩展性问题

| ID | 严重度 | 位置 | 描述 | 修复建议 |
|----|--------|------|------|----------|
| E-01 | P1 | 认证架构 | 无全局 JWT 中间件，每路由靠 `Depends(get_current_admin)`，新路由易漏 | `main.py` 默认拒绝 + 白名单（install/health/agent/auth/login） |
| E-02 | P2 | Agent 协议 | 全局单 Key + shell exec，难以多租户/多环境隔离 | per-server API key + scoped capabilities |
| E-03 | P2 | 配置三写 | install → `.env` + `config.php` + MySQL `settings`，漂移风险 | 单一真相源 + 同步脚本（已有部分，需校验任务） |
| E-04 | P3 | 前端 | 15 页独立 Alpine 内联脚本，无组件复用 | 逐步抽 `components/` 或轻量模块（非必须大重构） |

---

## 8. 优化点（非错误）

| ID | 位置 | 建议 |
|----|------|------|
| O-01 | [`server/api/search.py`](server/api/search.py) | LIKE 转义已做，可补全文索引或 ES（2000+ 资产搜索） |
| O-02 | [`web/app/layout.js`](web/app/layout.js) | 全局搜索已统一 `_esc()`，推广为唯一转义入口 |
| O-03 | [`server/background/heartbeat_flush.py`](server/background/heartbeat_flush.py) | 与 B-04 对齐后，可加批量大小/退避指标 |
| O-04 | 测试 | [`tests/`](tests/) 覆盖有限，优先补 auth/webssh/agent 集成测试 |
| O-05 | 文档 | 已有 [`docs/project/mysql-mcp-setup.md`](docs/project/mysql-mcp-setup.md)；安全项可增 ADR（WebSSH JWT、Agent exec） |

---

## 架构风险总览

```mermaid
flowchart TB
  subgraph attack [高优先级攻击面]
    XSS[XSS localStorage]
    WebSSH[IDOR WebSSH]
    AgentExec[Agent shell exec]
    APIKey[Global API_KEY reveal]
  end
  XSS --> TokenTheft[窃取 JWT]
  TokenTheft --> WebSSH
  TokenTheft --> APIKey
  APIKey --> AgentExec
```

---

## 修复优先级（建议执行顺序）

1. **立即（P0/P1 安全）**: S-01/B-01 WebSSH 绑定；S-04/S-05 XSS；S-02 Agent exec 收敛；S-03 API Key reveal；**S-19/S-20 sync_engine sysctl 注入**
2. **本周（一致性与性能）**: B-04/P-01 心跳写路径；B-03 在线统计；P-02 Redis pipeline
3. **迭代（纵深防御）**: S-06/S-07 会话与 WS；R-03/R-04 CDN/CSP；E-01 全局鉴权中间件
4. **文档/流程**: O-04 测试；R-08 依赖审计；每项修复写 [`docs/changelog/`](docs/changelog/)

---

## 与「WSL Node 20」计划的关系

| 计划项 | 状态 |
|--------|------|
| `wsl_install_node20.sh` 代理修复 | 已实现 |
| `MYSQL_READONLY=false` + `--writable` | 已实现 |
| `grant_nexus_local.sql` | 已实现 |
| MCP/Node 验证 | 已通过 |

**无需再跑该安装计划。** 若 MCP 仍红，仅需 Cursor Reload + 检查 MySQL 服务，而非重复改代码。

---

## 下一步（需你确认后执行）

若你确认本审计报告，可在 **Agent 模式** 按优先级分批修复（建议先做 P0/P1 安全项，每项单独 changelog）。你可指定：

- 只修安全（S-01 ~ S-12）
- 或「P0/P1 全部」
- 或指定 ID 列表

我不会再自动重复 WSL Node20 实施循环。

---

## 10. 如何强制「逐行审查」（Phase 2 操作规程）

初版（Phase 1）= **攻击面抽查 + 文件打勾**，不满足「逐行」承诺。要可复查、少漏报，必须单独开 **Phase 2**，并按下列规则验收。

### 10.1 你对助手说什么（复制即用）

切换到 **Agent 模式**（非 Plan），分批发：

```
执行审计计划 Phase 2：逐行复核。禁止攻击面抽查代替。

范围：server/**/*.py（本轮仅 server，完成后再 web）。

方法：
1. 用 rg 列出本目录每个文件的：命令拼接、subprocess、exec_ssh、WebSocket、SQL text(、硬编码密钥
2. 对每个命中行 Read 上下文 ±15 行，判定 safe / finding
3. 每个文件输出：
   - [path] 行数 N | rg 命中 M 条 | 全部 closure 后才能标「逐行完成 ✓」
   - findings 表：行号 | 严重度 | 描述
4. 禁止写「已审计」除非该文件 M=0 或每条命中都有 closure 记录
5. 每完成 5 个文件汇报一次，不要一次声称全库完成

从 server/application/services/sync_engine_v2.py 开始（已知漏报）。
```

修代码与审计**分两次会话**，避免混在一起。

### 10.2 「逐行」的可验收定义（不是读每一行散文）

| 必须做 | 说明 |
|--------|------|
| 全文 Read 或按 200 行分段 Read 至 EOF | 每个 `.py` 至少覆盖 100% 行号范围 |
| 全文件 rg 规则表（见 10.3） | 每个命中 = 一条待 closure 项 |
| 外部输入追调用链 | API → service → ssh/subprocess 到顶 |
| 产出 `docs/reports/audit-line-coverage-*.md` | 复查 AI 只认文件:行，不认「✓」 |

**不算逐行**：只 grep 几个关键词就标 ✓；只读 Top 15；子代理汇总不落行号。

### 10.3 每个 Python 文件必跑的 rg 规则（closure 前不得标完成）

```bash
# 在单个文件或目录执行，命中全部要 Read 上下文
rg -n "f[\"'].*\{.*\}.*(exec|run|shell|ssh|sysctl|curl|wget|sudo|bash|sh -c)" server/
rg -n "subprocess\.|create_subprocess_shell|os\.system|popen" server/
rg -n "exec_ssh_command|conn\.run\(|asyncssh" server/
rg -n "text\(|execute\(|raw\(" server/
rg -n "Depends\(get_current_admin\)|APIRouter|@router\.(get|post|put|delete|websocket)" server/api/
rg -n "HTTPException|401|403|API_KEY|compare_digest|secrets\." server/
```

前端另表：`innerHTML`、`localStorage`、`WebSocket.*token`、`eval(`。

### 10.4 推荐拆分顺序（避免一次吹「全库完成」）

| 批次 | 路径 | 约行数 |
|------|------|--------|
| 2a | `server/api/*.py` | 高 |
| 2b | `server/application/services/*.py` | 高（含 sync_engine） |
| 2c | `server/infrastructure/**/*.py` | 中 |
| 2d | `server/background/*.py` + `main.py` + `config.py` | 中 |
| 2e | `web/app/*.{html,js}` | 高 |
| 2f | `web/agent/*` + `scripts/*` + `tests/*` | 中 |

每批结束交付：`audit-phase-2{X}.md`（仅 findings + closure 表）。

### 10.5 你可加的 Cursor 规则（防止再抽查）

在 `.cursor/rules/audit-line-review.mdc` 中写：

- 安全审计任务：**禁止**在未列出 `文件:行号` 的情况下声称完成
- **禁止**使用「已审计 ✓」除非附带 rg 命中 closure 表
- 发现命令拼接必须引用 `shlex`/argv 或标为 finding
- 单次回复最多声称完成 **5 个文件** 的逐行审查

### 10.6 你怎么验收助手是否真的逐行

1. 抽查：打开 `sync_engine_v2.py`，看报告是否含 **234、236、284**（S-19/S-20）
2. 要求给 `wc -l` 与「本文件 Read 行范围 1-N」
3. 要求 `rg 命中数 = closure 数`，差 1 条即打回
4. 用另一只 AI 只喂 `audit-phase-*.md` + 源码，做交叉验证

### 10.7 Phase 2 todos（待你确认后执行）

| id | 内容 |
|----|------|
| phase2-api | `server/api/*.py` 全文 + rg closure |
| phase2-services | `server/application/services/*.py` 含 sync_engine 全行 |
| phase2-infra | `server/infrastructure/**` |
| phase2-web | `web/app` innerHTML + token |
| phase2-report | 合并为 `docs/reports/audit-line-coverage-2026-05-22.md` |

---

## 11. 逐行走读标准（SSOT — 唯一验收依据）

**独立文件（已落地）**：

| 文件 | 用途 |
|------|------|
| [`docs/project/line-walk-audit-standard.md`](docs/project/line-walk-audit-standard.md) | 完整标准（推荐给人与复查 AI） |
| [`.cursor/rules/audit-line-review.mdc`](.cursor/rules/audit-line-review.mdc) | Cursor 编辑 `server/`/`web/` 时自动加载 |

本节是 Phase 2 的**正式标准**（与上表同步）。与 Phase 1（攻击面抽查）严格区分；未满足 **DoD** 的文件不得标记「逐行完成」。

### 11.1 术语

| 术语 | 定义 |
|------|------|
| **走读** | 对单文件按行号顺序阅读源码（可分段），并对规则命中点做上下文分析 |
| **触达** | 仅打开/ grep 过文件，未满足走读 DoD |
| **规则命中** | `rg` 规则表（11.4）在某行产生的匹配 |
| **Closure** | 对一条规则命中的判定记录：`SAFE` 或 `FINDING`（含行号、理由） |
| **DoD** | 单文件完成定义（11.6），全部满足才可标「逐行完成 ✓」 |

### 11.2 适用范围

| 包含 | 排除 |
|------|------|
| `server/**/*.py` | `docs/**` 仅做密钥扫描（11.8） |
| `web/app/*.{html,js}` | `.cursor/skills/**` |
| `web/agent/**` | 第三方 minified 库 |
| `scripts/**`、`deploy/**`、`tests/**` | 图片、字体、lock 文件 |

### 11.3 单文件走读流程（强制 8 步）

对清单中**每一个**源文件依次执行，不得跳步：

```
Step 1  登记：记录 path、语言、wc -l 行数 N
Step 2  全文覆盖：Read offset=1, limit=200 循环至 EOF（末段 offset 必须 ≥ N-5）
Step 3  规则扫描：对该文件跑 11.4 中「本语言」全部规则，得到命中列表 H[]
Step 4  Closure：对 H[] 中每条命中 Read ±15 行，填写 11.5 表格一行
Step 5  入口梳理：若为 API/路由/WS，列出对外入口表（方法、路径、鉴权方式）
Step 6  数据流：标出「外部输入 → 危险 sink」（SQL/shell/文件/innerHTML/出站请求）
Step 7  八类核对：按 11.7 勾选本文件涉及的类别（无则勾 NONE）
Step 8  DoD 检查：满足 11.6 后写入报告，标记「逐行完成 ✓」
```

**单会话上限**：最多完成 **5 个文件** 的 Step 1–8，防止敷衍。

### 11.4 规则表（必须 100% Closure）

#### Python（`server/`、`scripts/`、`tests/`、`web/agent/*.py`）

| 规则 ID | rg 模式（示意） | 审查要点 |
|---------|----------------|----------|
| PY-01 | `f["'].*\{.*\}.*` 且行内含 exec/run/shell/ssh/sysctl/curl/sudo/bash | 是否经 shell？变量是否 shlex.quote **整参数**？ |
| PY-02 | `subprocess\.|create_subprocess_shell|os\.system|popen` | shell=True？命令来源？ |
| PY-03 | `exec_ssh_command|conn\.run\(` | asyncssh 是否默认 shell？拼接方式？ |
| PY-04 | `text\(|execute\(|raw\(` | SQL 是否拼接用户输入？ |
| PY-05 | `@router\.|APIRouter|websocket` | 是否有 `Depends(get_current_admin)` 或等效？ |
| PY-06 | `API_KEY|SECRET|password|token` 字面量 | 是否硬编码密钥？ |
| PY-07 | `pickle|yaml\.load\(|eval\(|exec\(` | 反序列化 / 动态执行 |
| PY-08 | `==.*[Kk]ey|!=.*[Kk]ey` | API Key 比较是否 timing-safe |
| PY-09 | `except.*:.*pass|except Exception` | 是否吞掉安全相关异常 |
| PY-10 | `open\(|Path\(.*write|unlink` | 路径遍历、任意写 |

#### 前端（`web/app/*.{html,js}`）

| 规则 ID | rg 模式 | 审查要点 |
|---------|---------|----------|
| FE-01 | `innerHTML|outerHTML|insertAdjacentHTML` | 每个 `${}` 是否 esc / textContent |
| FE-02 | `localStorage|sessionStorage` | 是否存 JWT/密钥 |
| FE-03 | `WebSocket.*token|\.token=` | Token 是否在 URL |
| FE-04 | `fetch\(|apiFetch` | 是否绕过统一鉴权/错误处理 |
| FE-05 | `onclick=.*\$\{|javascript:` | 属性上下文注入 |
| FE-06 | `cdn\.|script src=` | SRI、供应链 |

#### Shell / SQL / Deploy

| 规则 ID | 范围 | 要点 |
|---------|------|------|
| SH-01 | `*.sh` | 未引用变量、curl 明文密钥 |
| DP-01 | `deploy/nginx*.conf` | CSP、TLS、限流 |
| SQL-01 | `*.sql` | GRANT 过大、'%' host |

**无命中**：在 Closure 表写 `H=0（规则表零命中）`，仍须完成 Step 2 全文 Read。

### 11.5 Closure 记录格式（每条命中一行）

```markdown
| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由（≤1句） |
|------|------|------|------|--------|--------------|
| sync_engine_v2.py | 236 | PY-01 | FINDING | P1 | sysctl -w 仅 quote value，' 可断 shell |
| sync_engine_v2.py | 95 | PY-01 | SAFE | — | rsync 路径双端 shlex.quote |
```

- **SAFE** 必须写清依据（如「整段 argv」「Pydantic 校验」「仅常量」）
- **FINDING** 必须对应 11.7 八类之一，并给修复方向（≤1 句）

### 11.6 单文件 DoD（全部打勾才算完成）

- [ ] Step 2：Read 覆盖行号 `1 .. N`（报告注明 N 与最后 Read 的 offset/limit）
- [ ] Step 3–4：`len(H) == Closure 行数`（含 SAFE + FINDING）
- [ ] Step 5：有对外入口则必有「入口表」；纯 `__init__.py` 可写 N/A
- [ ] Step 6：已标外部输入与 sink，或写「无外部输入」
- [ ] Step 7：八类核对已勾
- [ ] 报告含「逐行完成 ✓」且**无** Phase 1 式笼统描述

### 11.7 问题八类与严重度（与 KPI 对齐）

| 类别 | 代号 | P0 示例 | P1 示例 | P2 示例 |
|------|------|---------|---------|---------|
| 功能缺陷 | BUG | — | 统计与列表数据源不一致 | 空 catch 吞错误 |
| 安全漏洞 | VULN | 未授权 RCE、IDOR 拿 Shell | XSS、密钥回读、命令注入 | CSRF 缺失 |
| 规范违规 | STYLE | — | 违反项目 esc 规则 | 重复 esc 实现 |
| 安全风险 | RISK | 仓库内生产 API Key | 明文 HTTP 调 Agent、弱 crypto | 无 SRI |
| 性能 | PERF | — | 心跳直写 MySQL、Redis N+1 | 阻塞 subprocess |
| 可用性 | UX | — | — | 加载失败无提示 |
| 可扩展性 | EXT | — | 无全局鉴权中间件 | 配置三写漂移 |
| 优化点 | OPT | — | — | 可合并重复逻辑 |

**KPI（复查用）**：P0 漏报 -100 / P1 漏报 -50；每条 FINDING 必须 `文件:行号` 可定位。

### 11.8 批次交付物模板

每个批次一个文件：`docs/reports/audit-phase-2{X}-line-walk.md`

```markdown
# Phase 2{X} 逐行走读 — {目录}

## 文件清单
| 文件 | 行数 N | 命中 H | Closure | 状态 |
|------|--------|--------|---------|------|

## Findings（仅 FINDING）
| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |

## Closure 全表（含 SAFE）
（11.5 格式）

## 入口表（api 批次必填）
| 方法 | 路径 | 鉴权 |

## 走读签字
- 范围：...
- 完成文件数 / 计划文件数
- 未含「攻击面 Top N」字样：是/否
```

### 11.9 禁止行为（违反即打回）

1. 未列 `文件:行号` 的 FINDING  
2. `rg 命中数 ≠ Closure 数` 仍标完成  
3. 用子代理「Top 15」代替 Closure 全表  
4. 只写「已审计 ✓」无 Step 2 行号范围  
5. 单回复声称完成整库 `server/` 或全部 `web/`  
6. 审计与修代码同一任务混合（除非用户明确要求）

### 11.10 与 Phase 1 的关系

| Phase 1 | Phase 2 |
|---------|---------|
| 攻击面抽查、快速风险图 | 逐行走读、可复查 closure |
| 可发现「显眼」P0 | 可发现「单文件局部」拼接类 P1（如 sysctl） |
| 报告在计划 §0–§8 | 报告在 `docs/reports/audit-phase-2*.md` |

**原则**：Phase 1 的 FINDING 在 Phase 2 须逐条 **VERIFY**（行号仍成立或降级/关闭）；Phase 2 可新增 FINDING，不得删减已确认行号而不说明。
