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