# 外网攻击面安全巡检报告（E1–E6）

**日期**：2026-06-06  
**范围**：生产 `https://api.synaglobal.vip` 无 JWT / 弱认证暴露面  
**信任边界**：已登录管理员视为可信；本轮不审计持 JWT 后的管理操作  
**模式**：audit_only（仅报告与 backlog，**本轮不修改应用代码**）  
**黑盒证据**：[`2026-06-06-external-attack-probe-production.md`](2026-06-06-external-attack-probe-production.md)

---

## 1. 攻击面资产表（E1）

来源：`server/api/auth_jwt.py` `PUBLIC_PREFIXES`、`server/main.py` 路由与静态挂载、OpenAPI 交叉核对。

### 1.1 JWT 中间件跳过路径（`PUBLIC_PREFIXES`）

| 前缀/路径 | 方法 | 二次鉴权 | 说明 |
|-----------|------|----------|------|
| `/api/auth/login` | POST | 无 | 暴破/枚举面；15min 锁定 |
| `/api/auth/refresh` | POST | HttpOnly cookie | 刷新令牌 |
| `/api/auth/logout` | POST | body refresh | 吊销 refresh |
| `/api/agent/*` | POST | `X-API-Key` + per-server | heartbeat、script-callback |
| `/api/install/*` | GET/POST | 安装锁 + install_token | 已锁定除 status 外 403 |
| `/api/settings/bing-wallpaper(s)` | GET | 无 | 只读壁纸 URL |
| `/api/settings/bing-wallpapers/sync` | POST | **应为 JWT**；见 §2 FINDING | 前缀匹配导致误放行 |
| `/health` | GET | 无 | 纯文本 `ok` |
| `/health/detail` | GET | **应为 JWT**；见 §2 FINDING | 前缀 `/health` 误匹配 |
| `/ws/*` | WS | Query JWT / WebSSH token | ADR-011 |
| `/openapi.json` `/docs` `/redoc` | GET | 无 | FastAPI 默认文档 |
| 其余 `/api/*` | * | JwtAuthMiddleware Bearer | 401 无 token |

**注意**：`JwtAuthMiddleware` 仅拦截 `path.startswith("/api/")`；`/health/detail` 不走中间件，完全依赖 `Depends(get_current_admin)`，与 `PUBLIC_PREFIXES` 前缀匹配叠加后失效。

### 1.2 静态与前端壳

| 路径 | 认证 | 文件 |
|------|------|------|
| `/app/*` | 无（SPA）；`AppAuth` 仅校验 refresh cookie 格式 | `web/app/` |
| `/agent/install.sh` `agent.py` `agent.sh` `uninstall.sh` | 无 | `web/agent/` |
| `/app/install.html` | 安装模式；锁定后归档为 `.bak` | 安装向导 |

### 1.3 Install API 端点（`prefix=/api/install`）

| 端点 | 方法 | 已锁定生产实测 |
|------|------|----------------|
| `/status` | GET | **404**（`_is_locked()`） |
| `/env-check` | GET | 403 |
| `/connection-check` | GET | 403 |
| `/state` | GET | 403 |
| `/test-credentials` | POST | 403（合法 body） |
| `/init-db` | POST | 403 |
| `/create-admin` | POST | 403 + install_token |
| `/lock` | POST | 403 |

### 1.4 Agent API

| 端点 | 认证链 |
|------|--------|
| `POST /api/agent/heartbeat` | Header `X-API-Key` → `_verify_server_api_key(server_id)` |
| `POST /api/agent/script-callback` | 同上 + `job_id`/`secret` + Redis 限速 |

控制面**无** `/api/agent/exec`（P0 RCE 已移除，注释见 `agent.py:378`）。

---

## 2. 生产黑盒探测（E2）

见 [`2026-06-06-external-attack-probe-production.md`](2026-06-06-external-attack-probe-production.md)。

### FINDING 汇总

| ID | 级别 | 问题 | 根因（代码） |
|----|------|------|--------------|
| EXT-01 | **P2** | `GET /health/detail` 无 JWT 返回组件状态 | `PUBLIC_PREFIXES` 含 `/health`，`_is_public_path` 使 `get_current_admin` 无凭证返回 `None`；handler 未校验 admin |
| EXT-02 | **P2** | `POST /api/settings/bing-wallpapers/sync` 无 JWT 可触发 Bing 下载 | 同上：`/api/settings/bing-wallpapers` 前缀匹配 `/sync` 子路径 |
| EXT-03 | **H** | `/openapi.json` `/docs` `/redoc` 公网可访问 | `main.py` 未设 `docs_url=None`；前缀在 PUBLIC |
| EXT-04 | **H** | 外网 WS 握手 HTTP 403，无法验证 4001/4401 | 反代/Nginx 规则；应用层逻辑见 `websocket.py` |
| — | PASS | 管理 API、假 Agent Key、锁定 install | JwtAuthMiddleware / `_reject_if_locked` |

**P0/P1：0**（无未授权 RCE、写库、读密文、重装成功）。

---

## 3. Agent M2M 滥用链（E3）

### 3.1 认证逻辑

```mermaid
sequenceDiagram
    participant Attacker
    participant API as Nexus_API
    participant Redis
    participant WS as WebSocket_clients

    Note over Attacker: 持有泄露的 global API_KEY
    Attacker->>API: POST /api/agent/heartbeat<br/>X-API-Key + server_id=N
    API->>API: server 存在且无 agent_api_key?
  alt legacy 服务器
        API->>Redis: HSET heartbeat:N
        API->>WS: broadcast_alert 若超阈值
        API-->>Attacker: 200 ok
    else 已有 per-server key
        API-->>Attacker: 401
    end
```

- **全局 Key 回退**：~~`server/api/agent.py:84-92`~~ **已移除（BL-06，2026-06-07）** — Agent 仅接受 per-server `agent_api_key`。
- **影响**：伪造心跳需持有目标服务器的 per-server key；global `API_KEY` 不再通行。
- **script-callback**：需有效 `job_id` + `secret`（一次性）+ per-server key；`check_script_callback_rate` 每 job 10/min、每 IP 60/min（`script_callback_rate.py`）。
- **生产探测**：假 Key 或 global Key → **401**。

**与 risk-acceptance 关系**：接受项 2 已于 2026-06-07 收口，见 [`risk-acceptance-single-operator.md`](../project/risk-acceptance-single-operator.md) §接受项 2。

---

## 4. 安装向导边界（E4）

### 4.1 令牌逻辑（代码）

- `_verify_install_token`：`secrets.compare_digest` + 状态文件 `install_token`（`install.py:933-946`）。
- 无状态文件 → 403；暴力需猜 256-bit 级 token，**不可行**。
- `create-admin` / `lock` 需 token 或已完成步骤。

### 4.2 已安装 + 锁定（生产）

- **BL-05（2026-06-07）**：全部 `/api/install/*` → **404** `Not found`（含 status、env-check、init-db、lock 等），无中文指纹。
- **无法**外网重装（init-db/create-admin 被锁）。

### 4.3 残余 H

- 未锁定环境下 `/status` 会返回 `installed/locked/configured`（仅安装模式相关）。
- OpenAPI 仍暴露 install 路径 schema（EXT-03）。

---

## 5. 认证与会话（E5）

| 项 | 结论 |
|----|------|
| 登录暴破 | `MAX_LOGIN_FAILURES` + 15min 锁定（`auth_service.py:122-130`）；**本轮未跑循环暴破** |
| IP allowlist | **仅 `login()`**（`auth_service.py:98-120`）；拿到 JWT 后**不受** allowlist 约束 |
| 用户名枚举 | 生产单次错误登录：`用户名或密码错误`（不存在与密码错误文案统一）— PASS |
| Refresh | HttpOnly + Secure + SameSite=Lax；重用检测见 `tests/test_auth_refresh_reuse.py` |
| Logout CSRF | SameSite=Lax 降低跨站 POST 风险 |

**外网主要面**：`/api/auth/login` 暴破（有锁定）、refresh cookie 窃取（需 XSS/本机恶意软件）。

---

## 6. 总结与验收

| 验收项 | 状态 |
|--------|------|
| 生产黑盒脚本 + 证据 | ✅ `scripts/security_probe_external.sh` + probe 报告 |
| PUBLIC_PREFIXES + static 覆盖 | ✅ §1 |
| 0 未解释 P0/P1 | ✅ |
| 加固 backlog 独立 | ✅ [`2026-06-06-external-attack-hardening-backlog.md`](2026-06-06-external-attack-hardening-backlog.md) |

### 分级统计

- **P0**：0  
- **P1**：0  
- **P2**：2（EXT-01、EXT-02）  
- **H**：2+（EXT-03、EXT-04、OpenAPI 指纹、Agent legacy 已接受）

### 与 B1–B6 API 巡检关系

| 已完成 B 轮 | 本轮 E 轮 |
|-------------|-----------|
| 鉴权后逻辑、输入上限 | 无 JWT 暴露面 |
| 当场修复若干 schema 上限 | audit_only，修复进 backlog |

---

## 7. 加固项状态（2026-06-07 更新）

| ID | 项 | 状态 |
|----|-----|------|
| BL-06 | Agent per-server key 迁移 | **已实施**（2026-06-07）：移除 global `API_KEY` 回退，强制 per-server `agent_api_key`；见 changelog `2026-06-07-agent-per-server-key-enforced.md` |
| BL-07 | WebSocket 探测口径 | **已接受**：无 token 外网 HTTP 403 为应用拒绝握手；登录后 WS 正常；见 deploy-verification §WebSocket |
| BL-08 | 全站 IP 白名单 | **不需要**（运维确认不实施；维持仅 login allowlist） |

**已实施加固**：BL-01～BL-06（见 backlog 与 changelog）。

---

**下一步**：部署后重跑 `security_probe_external.sh`；新增第一台子机时走「创建 → 远程安装 Agent」验证 per-server 心跳。
