Files
Nexus/docs/audit/2026-06-12-remote-browser-worker.md
T
Nexus Agent dbb6bc4a55 fix(browser): 门控 — Worker TLS verify 可配置、审计清单与 shell LF
默认校验 Worker HTTPS 证书;审计文件补全变更清单;docker-entrypoint.sh 改为 LF。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 06:50:06 +08:00

143 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审计 — 远程浏览器(Playwright Worker
**Changelog**: `docs/changelog/2026-06-12-remote-browser-worker-deploy.md`
**设计 SSOT**: `docs/design/specs/2026-06-11-server-browser-chromium-design.md`
**范围**: 2026-06-12 新增/恢复的浏览器 Worker 全链路(未含 `web/app` 构建产物)
## 范围(文件清单)
| 文件 | 职责 |
|------|------|
| `browser-worker/Dockerfile` | Worker 镜像 |
| `browser-worker/docker-compose.yml` | Worker 编排 |
| `browser-worker/docker-entrypoint.sh` | headed + Xvfb 入口 |
| `browser-worker/requirements.txt` | Worker 依赖 |
| `browser-worker/config.py` | Worker 环境配置 |
| `browser-worker/main.py` | Worker HTTP/WS API |
| `browser-worker/session_manager.py` | Playwright + screencast |
| `browser-worker/browser_stealth.py` | 反自动化 / 验证码辅助 |
| `browser-worker/browser_url_safe.py` | SSRFWorker 侧) |
| `server/main.py` | 注册 browser 路由 |
| `server/config.py` | `BROWSER_*` 与 TLS verify |
| `server/api/settings.py` | 设置项默认值(push_complete_sound |
| `server/api/browser_session.py` | 桥接 REST + `/ws/browser` |
| `server/api/browser.py` | UI 状态 `/api/browser/state` |
| `server/infrastructure/browser/__init__.py` | 包入口 |
| `server/infrastructure/browser/worker_client.py` | 连 Worker(可配置 TLS verify |
| `server/infrastructure/browser/session_registry.py` | 内存会话归属 |
| `server/utils/browser_url_safe.py` | SSRF(桥接层) |
| `server/utils/browser_ui_state.py` | 状态解析 |
| `frontend/src/App.vue` | 顶栏 + 固定 GlobalBrowserPanel |
| `frontend/src/router/index.ts` | `/browser` 路由 |
| `frontend/src/pages/ServersPage.vue` | 服务器页打开浏览器 |
| `frontend/src/pages/BrowserPage.vue` | 独立浏览器页(兼容) |
| `frontend/src/components/GlobalBrowserPanel.vue` | 固定顶栏下整窗 |
| `frontend/src/composables/useGlobalBrowser.ts` | 全局标签/状态 |
| `frontend/src/composables/useRemoteBrowserSession.ts` | canvas + WS |
| `tests/test_browser_url_safe.py` | SSRF 单测 |
| `tests/test_browser_ui_state.py` | UI 状态单测 |
| `tests/test_settings_sound_defaults.py` | 设置默认音单测 |
## API / WS 入口表
| 方法 | 路径 | 鉴权 |
|------|------|------|
| GET | `/api/browser/status` | JWT `get_current_admin` |
| POST | `/api/browser/sessions` | JWT |
| DELETE | `/api/browser/sessions/{id}` | JWT + 会话归属 |
| GET/PUT | `/api/browser/state` | JWT + `admin_ui_preferences` |
| WS | `/ws/browser/{session_id}?token=` | JWT + 会话归属 |
| GET | Worker `/health` | **无** |
| POST/DELETE | Worker `/v1/sessions*` | `X-Nexus-Worker-Key` |
| WS | Worker `/v1/sessions/{id}/stream` | Worker KeyHeader |
## Step 3 规则扫描
| ID | 规则 | 结论 |
|----|------|------|
| H1 | SSRF / 私网访问 | **RISK** — 见 F1、F2 |
| H2 | 鉴权 / IDOR | PASS — WS 校验 admin_id |
| H3 | 密钥不落库/不入 git | PASS — `.env` / 持久卷 |
| H4 | 无静默吞错 | **RISK** — 见 F4 |
| H5 | Worker 暴露面 | **RISK** — 见 F3 |
| H6 | 多 worker 一致性 | **RISK** — 见 F5 |
| H7 | UI 状态 URL 白名单 | PASS — `browser_ui_state` |
| H8 | 设计符合度(redirect | **GAP** — 见 F1 |
## Closure(全表)
| ID | 判定 | 严重度 | 位置 | 说明 |
|----|------|--------|------|------|
| H1 | FINDING | **P1** | `browser-worker/session_manager.py` NAVIGATE 后 | **F1** 页内点击/重定向未逐跳 SSRF 校验;设计文档要求 redirect 后重验 |
| H1 | FINDING | **P1** | Worker 部署 ufw | **F3** 装机时额外 `ufw allow 8443/tcp`(全网),削弱「仅主站 IP」隔离 |
| H2 | SAFE | — | `browser_session.py:125-127` | `session_id` 必须属于当前 `admin.id` |
| H2 | SAFE | — | `browser_session.py:95-104` | 桥接层 NAVIGATE 二次校验 |
| H3 | SAFE | — | `worker_client.py`, Worker `.env` | `WORKER_SECRET` 仅环境变量 |
| H4 | FINDING | P2 | `worker_client.py:67-68` | **F4** `delete_worker_session` 网络失败仅 warning,可能残留 Worker 会话 |
| H5 | FINDING | P2 | `browser-worker/main.py:45-51` | `/health` 无认证;若 8443 对公网可见会泄露会话数 |
| H6 | FINDING | P2 | `session_registry.py` | **F5** 内存注册表不跨 uvicorn worker;多进程下 WS 可能 4403 |
| H7 | SAFE | — | `browser_ui_state.py` | http(s) only,无凭据 URL |
| H8 | SAFE | — | screencast 热修 | `cdp.on("Page.screencastFrame")` 已替代错误 API |
| — | SAFE | — | `browser_session.py:61-64` | 创建前清理陈旧会话,避免 409 |
| — | SAFE | — | 前端 | 不再 iframecanvas 远程渲染 |
| — | SAFE | — | CUD 审计 | 首版未记 session 审计,与设计「与 RDP 一致」一致 |
### F1 — 重定向 / 页内导航 SSRF 缺口(P1)
`validate_navigation_url` 仅在显式 `NAVIGATE` 消息时调用。用户在远程 Chromium 内**点击链接**或 **302 跳转**到 `http://10.x` / metadata 时,Playwright 会照常请求,**不经过**校验。
**建议**`page.on("framenavigated")` / `response` 监听,每跳对 `page.url` 调用 `validate_navigation_url`;失败则 `page.close()``about:blank` 并 WS `ERROR`
### F3 — Worker 防火墙过宽(P1
部署日志显示除 `from 20.24.218.235` 外还有 `ufw allow 8443/tcp`Anywhere)。攻击者若猜到 `WORKER_SECRET`(或暴力)可直连 Worker。
**建议**:删除全网 8443 规则,仅保留主站源 IP;`/health` 改内网或加 Key。
### F4 — Worker 删除失败(P2
`delete_worker_session` 异常时静默;依赖 Worker 空闲超时清理。
### F5 — 多 uvicorn workerP2
`session_registry` 进程内字典;生产若 `--workers>1` 可能 POST 在 worker A、WS 落到 worker B。当前 Docker 单进程可接受;扩 worker 时需 Redis 注册表。
## 外部输入 → Sink
| 输入 | Sink | 缓解 |
|------|------|------|
| `NAVIGATE.url` | Playwright `goto` | 双端 `validate_navigation_url` |
| 页内链接点击 | Playwright 导航 | **未缓解(F1** |
| WS `MOUSE`/`KEY` | Chromium | 仅已认证管理员 |
| `browser/state` tabs URL | MySQL JSON | `_is_safe_url` 过滤 |
| Worker Key | Worker API | 常量时间比较(`!=` |
## DoD
| 项 | 状态 |
|----|------|
| `pytest tests/test_browser_url_safe.py tests/test_browser_ui_state.py` | 7 passed(生产机);本地沙箱 DNS 失败属环境限制 |
| `npm run type-check` | 已通过(实现日) |
| 生产 `GET /api/browser/status` | `enabled:true`, `worker_ok:true` |
| 生产 screencast 冒烟 | `SCREENCAST_OK` ~4KB JPEG |
| changelog | `docs/changelog/2026-06-12-remote-browser-worker-deploy.md` |
## 总结
| 级别 | 数量 | 处置建议 |
|------|------|----------|
| P0 | 0 | — |
| P1 | 2 | F1 redirect/页内 SSRFF3 收紧 ufw |
| P2 | 3 | F4/F5 可排期;/health 加固 |
**合并结论**:功能链路可用,鉴权与显式 NAVIGATE SSRF 达标;**未达设计文档「每跳 redirect 校验」**,且 Worker **8443 应对公网收口**。建议先修 F3(运维),再实现 F1(代码)后复审计。
## 验证命令
```bash
venv/bin/pytest tests/test_browser_url_safe.py tests/test_browser_ui_state.py -q
cd frontend && npm run type-check
# 生产
curl -H "Authorization: Bearer $TOKEN" https://api.synaglobal.vip/api/browser/status
```