@@ -0,0 +1,608 @@
---
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,不得删减已确认行号而不说明。