Files
Nexus/docs/reports/2026-06-06-external-attack-surface-audit.md
T

188 lines
8.0 KiB
Markdown
Raw Normal View History

# 外网攻击面安全巡检报告(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/P10**(无未授权 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-062026-06-07** — Agent 仅接受 per-server `agent_api_key`
- **影响**:伪造心跳需持有目标服务器的 per-server keyglobal `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-052026-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 路径 schemaEXT-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**2EXT-01、EXT-02
- **H**2+EXT-03、EXT-04、OpenAPI 指纹、Agent legacy 已接受)
### 与 B1B6 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-01BL-06(见 backlog 与 changelog)。
---
**下一步**:部署后重跑 `security_probe_external.sh`;新增第一台子机时走「创建 → 远程安装 Agent」验证 per-server 心跳。