# 审计 — 远程浏览器(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` | SSRF(Worker 侧) | | `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 Key(Header) | ## 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 | — | 前端 | 不再 iframe;canvas 远程渲染 | | — | 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 worker(P2) `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/页内 SSRF;F3 收紧 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 ```