# Phase 2a 逐行走读 — vendor / CDN / install

- **日期**: 2026-06-04
- **审计人**: Cursor Agent
- **触发**: 用户要求全面逐步审查；本批为 CDN 自托管 + install B904 相关

## 审计范围（Batch 2a，5 文件）

| 文件 | N 行 | Read 范围 | 状态 |
|------|------|-----------|------|
| `server/api/install.py` | 695 | 1–695 全文 | ☑ |
| `web/app/api.js` | 209 | 1–209 全文 | ☑ |
| `scripts/sync_frontend_vendor.py` | 180 | 1–180 全文 | ☑ |
| `tests/test_security_unit.py` | 188 | 1–188 全文 | ☑ |
| `web/app/preview.html` | 183 | 1–183 全文 | ☑ |

---

## Step 3 规则扫描 H

### install.py (PY)

| ID | 命中行 | 说明 |
|----|--------|------|
| PY-01 | 267-287 | subprocess 列表 argv |
| PY-04 | 438-646 | `text()` + 绑定参数 / 常量 SQL |
| PY-05 | 297-695 | 安装 API 无 JWT（设计如此） |
| PY-06 | 131-133 | 生成密钥写入 .env，非硬编码 |
| PY-09 | 439-450, 600-601, 694-695 | `except: pass` / 宽 except |
| PY-10 | 160, 187, 239, 256, 665 | 写文件路径来自 ROOT_DIR |

### api.js (FE)

| ID | 命中行 |
|----|--------|
| FE-01 | 183 |
| FE-02 | 8-122 |
| FE-04 | 22, 59, 94 |
| FE-06 | 140-143（已本地 vendor） |

### sync_frontend_vendor.py

| ID | 命中行 |
|----|--------|
| PY-10 | 96-102 写 vendor |
| — | urllib 下载（开发工具） |

### test_security_unit.py

规则零命中（测试代码，无生产 sink）

### preview.html

| ID | 命中行 |
|----|--------|
| FE-06 | 7（本地 vendor） |
| FE-05 | 92 `onclick` 常量 |

---

## Step 4 Closure 表（全表）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| install.py | 39-41 | PY-10 | **FINDING** | P1 | `_is_installed()`=`.env`∨锁；step3 写 `.env` 后 step4/5 被 `_reject_if_installed` 403，与 install.html 流程矛盾 |
| install.py | 306-312 | PY-05 | **FINDING** | P1 | 同上，阻断 `create-admin`/`lock` |
| install.py | 438-450 | PY-09 | SAFE | — | DDL 幂等，注释 noqa S110 |
| install.py | 487-491 | PY-04 | SAFE | — | `:k/:v` 参数化 |
| install.py | 562-574 | PY-04 | **FINDING** | P2 | INSERT 未含 `is_active`；依赖 DB 默认，与 lock 查询 `is_active=1` 可能不一致 |
| install.py | 645-662 | PY-09 | SAFE | — | 503 `from e`，B904 已修 |
| install.py | 267-287 | PY-01 | SAFE | — | `subprocess.run([...])` 无 shell |
| install.py | 179 | PY-06 | **FINDING** | P2 | `config.json` 存 `db_pass` 明文（安装三写之一，内网仍属风险） |
| install.py | 694-695 | PY-09 | RISK | P2 | `/state` 解析失败静默回 step1，可接受 |
| api.js | 8-30 | FE-02 | RISK | P2 | access_token 在 localStorage（项目既定）；refresh 为 HttpOnly cookie |
| api.js | 183,209 | FE-01 | SAFE | — | `message` 经 `_toastEsc` |
| api.js | 99 | PY-09 | SAFE | — | logout 通知 best-effort 空 catch |
| api.js | 136-149 | FE-06 | **FINDING** | P2 | 无 `s.onerror`；404 时 `_monacoLoading` 不重置 |
| api.js | 140-143 | FE-06 | SAFE | — | 已 `/app/vendor/monaco/vs` |
| sync_frontend_vendor.py | 85-102 | PY-10 | **FINDING** | P2 | `rel` 未拒绝 `..`；恶意 tgz 可路径穿越（仅维护脚本） |
| sync_frontend_vendor.py | 125-127 | — | SAFE | — | 固定 URL 列表下载 |
| test_security_unit.py | 131-163 | FE-06 | SAFE | — | CDN 回归测试合理 |
| preview.html | 7 | FE-06 | SAFE | — | 本地 tailwind vendor |
| preview.html | 全文 | FE-01 | SAFE | — | 静态 mock，无动态 innerHTML |

---

## Step 5 入口表（install.py）

| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| GET | `/api/install/status` | 无 | 可查已安装/锁定 |
| GET | `/api/install/env-check` | 无 | 安装中环境检测 |
| POST | `/api/install/init-db` | 无 | Step3，写 `.env` |
| POST | `/api/install/create-admin` | 无 | Step4 |
| POST | `/api/install/lock` | 无 | Step5 写 `.install_locked` |
| GET | `/api/install/state` | 无 | 向导状态 |

安装模式设计：无 JWT；依赖「未完成锁定前」网络隔离。

---

## Step 6 输入 → Sink

| 文件 | 外部输入 | Sink | 防护 |
|------|----------|------|------|
| install.py | Pydantic `InitDbRequest`/`CreateAdminRequest` | MySQL `text(:param)`、`.env`、`config.json`、subprocess | `quote_plus`、`_escape_env_value`；subprocess argv |
| api.js | API JSON、`toast(message)` | fetch、localStorage、innerHTML | Bearer 头；toast esc |
| sync_frontend_vendor.py | npm/CDN URL（脚本常量） | 写 `vendor/` | 固定 manifest |
| preview.html | 无（静态） | — | — |

---

## Step 7 归类统计

```
install.py           695行  11H  4FINDING
api.js               209行   4H  1FINDING
sync_frontend_vendor 180行   2H  1FINDING
test_security_unit   188行   0H  0FINDING
preview.html         183行   2H  0FINDING
────────────────────────────────────────
总计                 5文件  19H  6FINDING（去重后 5 项）
```

八类：BUG×3 · VULN×0 · STYLE×0 · RISK×2 · PERF×0 · UX×1 · EXT×0 · OPT×0

---

## Step 8 DoD

- [x] 5 文件 Read 覆盖 1..N
- [x] len(H) == Closure 行数（19 行）
- [x] FINDING 均含 `文件:行号`
- [x] API 批次含入口表
- [ ] **批次结论**：❌ 5 FINDING，其中 **F-2a-01 (P1)** 影响全新安装向导

---

## FINDING 列表（修复建议）

### F-2a-01 · P1 · BUG · install.py:39-41,306-312,541,638

**现象**：Step3 `init-db` 写入 `.env` 后，`_is_installed()` 为真，`create-admin` 与 `lock` 均 `403`。

**建议**：拆分守卫——`init-db` 拒绝「已有 `.env`」；`create-admin`/`lock` 仅拒绝「已 `.install_locked`」；`GET /state` 在仅有 `.env` 时仍可读。

### F-2a-02 · P2 · UX · api.js:136-149

**建议**：`s.onerror` 将 `_monacoLoading=false` 并 `console.warn`；或移除无消费者的预加载（Vue 已 bundle Monaco）。

### F-2a-03 · P2 · BUG · install.py:562-574

**建议**：`INSERT` 增加 `is_active=1`（及 `ON DUPLICATE` 更新 `is_active=1`）。

### F-2a-04 · P2 · RISK · sync_frontend_vendor.py:96-97

**建议**：`if ".." in rel or rel.startswith("/"): continue`。

### F-2a-05 · P2 · RISK · install.py:179

**建议**：`config.json` 不存 `db_pass`，或安装完成后删除；文档标注风险。

---

## 结论（2026-06-04 更新）

☑ **F-2a-01～05 已修复** — 见 `docs/changelog/2026-06-04-install-wizard-guard-fix.md` 与 `audit-phase-3-delta-2026-06-04.md`。  
本批历史 FINDING 仅作记录；当前代码 **0 待办**。

登记册：`docs/audit/2026-06-04-full-line-walk-registry.md`
