接入 Playwright Worker、会话 WebSocket 与全局浏览器面板(固定于 App Bar 下); 含验证码 stealth、设置项默认音与 URL 安全校验;附带 worker 部署与设计文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
6.3 KiB
审计 — 远程浏览器(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/main.py |
147 | Worker HTTP/WS API |
browser-worker/session_manager.py |
323 | Playwright + screencast |
browser-worker/browser_url_safe.py |
79 | SSRF(Worker 侧) |
server/api/browser_session.py |
198 | 桥接 REST + /ws/browser |
server/infrastructure/browser/worker_client.py |
83 | 连 Worker |
server/infrastructure/browser/session_registry.py |
52 | 内存会话归属 |
server/utils/browser_url_safe.py |
80 | SSRF(桥接层) |
server/api/browser.py |
88 | UI 状态 /api/browser/state |
server/utils/browser_ui_state.py |
165 | 状态解析 |
frontend/src/composables/useRemoteBrowserSession.ts |
213 | canvas + WS |
frontend/src/components/GlobalBrowserPanel.vue |
380 | 浮动面板 |
tests/test_browser_url_safe.py |
32 | SSRF 单测 |
tests/test_browser_ui_state.py |
35 | UI 状态单测 |
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(代码)后复审计。
验证命令
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