Skip to content

fix(preview): isolate archived previews from studio sessions - #187

Open
AlexXPZhu wants to merge 1 commit into
Tencent:mainfrom
AlexXPZhu:fix/preview-session-isolation
Open

AlexXPZhu wants to merge 1 commit into
Tencent:mainfrom
AlexXPZhu:fix/preview-session-isolation

Conversation

@AlexXPZhu

Copy link
Copy Markdown

What changed

Fixes #93.

An archived preview could share the studio origin and use parent.fetch with the viewer's signed-in session. Previews now require a dedicated hostname and explicit studio frame origins.

  • Serve preview tokens only on the preview hostname; never route studio pages, API endpoints or MCP through that hostname. Reject the same hostname even with a different port.
  • Use the configured preview origin instead of request/forwarding headers. Reject studio hostnames as preview backends, including when reading previously persisted sessions.
  • Restrict resource requests as well as connections: preview content cannot send image/script/iframe requests to studio endpoints. Keep ES modules and the existing History API shim working on the isolated origin.
  • Add service/API regressions and a real Chromium test through the archive and platform gateway with a signed viewer cookie. The test bypasses the optional backend interceptor and checks parent access, native fetch, no-cors, images, modules, CSS, navigation/Back, and standalone viewing. Include it in CI's isolated browser-test step.

Scope

  • Platform core / API / runner
  • Public adapter / deployment
  • Classic
  • Portable
  • Shared behavior contract
  • Installer / CLI
  • Documentation only

Compatibility

Deployment configuration is required to reopen online previews:

AGENTFLOW_PREVIEW_PUBLIC_URL=https://preview.example.net
AGENTFLOW_PREVIEW_FRAME_ANCESTORS=https://studio.example.com

The second value accepts space-separated exact studio origins. Route both hostnames to the platform while preserving Host; deployment docs include proxy and local-development examples. Until configured, session creation returns an explicit error. Archiving and the authenticated ZIP download API remain available. Existing studio-origin preview links return 404.

Bundle external scripts/styles/fonts locally. Nested frames and workers/service workers are blocked. Configured non-studio backends still need their own CORS support. Preview producers share the dedicated preview origin; this change does not provide isolation between previews.

No SQL migration, schema change or runner protocol change. Classic and Portable are unchanged.

Validation

Passed on the submitted tree:

  • bash scripts/build-chatbot.sh: 299 tests and production build.
  • go test -p=2 ./... -count=1 -timeout=180s: all packages passed.
  • go vet ./...
  • python3 scripts/check-public.py
  • bash scripts/scan-secrets.sh
  • git diff --cached --check before committing.

Go tests used Go 1.26.11 and Chromium with its required system libraries. The Chromium regression passes both with and without a configured backend and in embedded/standalone modes.

Negative control: overlaying the four original production files from bdf8723 makes both API and Chromium regressions fail: studio APIs are exposed on the preview host, studio backends are accepted, and four image requests reach the studio token endpoint.

Not run: the full MySQL-backed public Playwright suite, Firefox/WebKit, or a live DNS/TLS proxy deployment. Local-edition checks are not applicable.

Safety

  • I did not include credentials, internal endpoints, production data, or organization-specific infrastructure.
  • I preserved user files and unrelated changes.
  • I updated generated Classic host bundles when applicable. — Not applicable.
  • I documented third-party content and licensing changes when applicable. — No new dependencies or third-party assets.

Prepared with Codex assistance.

Require a dedicated preview hostname and explicit studio frame origins.
Keep platform routes off the preview host and reject studio backends.
Cover authenticated browser isolation, module loading and SPA navigation.

@fx-kurumi fx-kurumi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 head 81c53b7:Request changes,工作台内嵌预览被平台自己的 CSP 拦截

隔离思路本身没问题:预览固定到独立主机名,不再读取 X-Forwarded-*;预览主机上的页面和 API 一律返回 404;已持久化的会话也会重新校验后端主机名。这些我都在本机确认过。问题出在这次没有改到的另一半:工作台页面本身的 CSP。

[P1] 预览移到独立主机名后,工作台页面的 frame-src 没有放行它,「前端预览」标签页里的 iframe 会被浏览器拦截

  • internal/httpapi/gin_helpers.go:127(不在本 PR diff 内):工作台页面的 CSP 是 frame-src 'self' blob:。以前预览和工作台同源,所以 'self' 足够。本 PR 把预览强制挪到 AGENTFLOW_PREVIEW_PUBLIC_URL 这个不同源的主机,web/app/runs.js:738 的 <iframe data-preview-frame src=…> 就会被工作台自己的 CSP 拦截。Chromium 报:Framing 'http://localhost:18187/' violates the following Content Security Policy directive: "frame-src 'self' blob:",iframe 内显示的是错误页。
  • 结果是按 PR 说明配好两个变量后,运行详情 →「前端预览」里看不到任何内容,只有「独立窗口」能用。这是这个功能的主要入口。
  • 测试没有发现这个问题:internal/httpapi/preview_isolation_browser_test.go:113 的 /viewer 父页面是测试自己写的 handler,响应里没有平台的 securityHeaders(),所以 iframe 能加载。真实工作台页面要经过 securityHeaders()。

建议:工作台 CSP 的 frame-src 追加配置好的预览 origin。例如在 securityHeaders() 里,当 preview.LoadConfig() 得到合法的独立 origin 时,在 frame-src 'self' blob: 后面追加 scheme://host。只需改 / 和 /index.html 等工作台页面,/chatbot/ 与 vscode 页面不嵌入预览,可以不改。同时请把浏览器回归测试的父页面改成真实的工作台页面(或者至少套上 securityHeaders()),让这个问题能被测试覆盖。

我用实验分支验证过:只在 securityHeaders() 的 frame-src 后追加预览 origin,其他不变,内嵌预览就能正常显示和交互。同时 parent.document 读取仍然被拦截(跨源),fetch 工作台 API 也仍然被拦截,隔离效果不受影响。

本机真实服务验证

环境:本机构建的平台 handler(httpapi.New 的完整中间件链)+ 真实预览服务(ArchiveLocal 归档、会话、网关),内存 sqlite,访客账号,虚构 run「评审用预览 SOP」;不连模型,不连外部服务。预览包是自己写的计数器页面:点 +1,并探测 parent.document、带 cookie 的 fetch 工作台 API、同主机的 /api。

内嵌「前端预览」标签页 独立窗口 parent.document fetch 工作台 API
base bdf8723(同源) 可显示,计数器可点 可用 可读(这正是 #93 的问题) 被拦截(connect-src 'none')
本 PR 81c53b7 被 frame-src 拦截,显示错误页 可用 —(iframe 未加载) 被拦截
本 PR + 上面的 frame-src 修改(实验) 可显示,计数器可点 可用 被拦截(跨源) 被拦截
本 PR:内嵌预览被拦截 base:内嵌预览正常(但同源) 本 PR + frame-src 修改

本 PR 的独立窗口(隔离生效,studioApi/sameHostApi 都被拦截):

交互录屏(打开运行 → 前端预览 → 点 3 次 +1 → 独立窗口):本 PR · base 对照 · 加上 frame-src 修改后

其他验证

  • go vet ./internal/preview ./internal/httpapi 通过;go test ./internal/preview 通过;go test ./internal/httpapi -run Preview 通过,包括 TestPreviewIsolationBrowser(本机非 root,需要给 Chromium 加 --no-sandbox)。
  • 预览主机上请求 /api/health 返回 404,符合预期。
  • go test ./internal/engine 有 2 个失败(TestPushConversationWorkspace…、TestPrepareRepositoryApplies…),原因是本机全局 git 身份干扰了测试中的提交作者,和本 PR 无关。
  • 非阻塞:web/app/runs.js:689 会对 http 预览地址用 HEAD 探测 https 版本,这个请求现在跨源,会被工作台的 connect-src 'self' 拦截,控制台多一条错误。它会回退到原地址,不影响功能。可以考虑只在工作台本身是 https 时才探测,或者直接信任服务端返回的地址。
  • 限制:没有跑 MySQL 版 Playwright 套件、Firefox/WebKit,也没有真实 DNS/TLS 反代部署。

var currentPreviewURL atomic.Value
var tokenRequests atomic.Int32
server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host == server.Listener.Addr().String() && r.URL.Path == "/viewer" {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的 /viewer 父页面由测试自己的 handler 返回,不带平台 securityHeaders() 的 CSP,所以测不出真实工作台 frame-src 'self' blob:(internal/httpapi/gin_helpers.go:127)会拦截跨源预览 iframe 的问题。建议让父页面走 handler(真实工作台页面),或者至少套上 securityHeaders()。

ancestors = "'self'"
} else if !strings.Contains(ancestors, "'self'") {
ancestors = "'self' " + ancestors
ancestors := "'none'"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

预览端的 frame-ancestors 已经放行工作台 origin,但嵌入方工作台页面的 frame-src 还是 'self' blob:,两边要同时放开。建议在 securityHeaders() 里,当 AGENTFLOW_PREVIEW_PUBLIC_URL 是合法的独立 origin 时,在 frame-src 后追加这个 origin(我在本机实验过,内嵌预览能恢复,parent.document 仍然被拦截)。

@Robert0622

Copy link
Copy Markdown
Collaborator

LoopForge PR Review — head 81c53b7cef65ea7dc63afcbc772a60041852f5ca

Verdict: ⚠️ Verification blocked

  • Scope: 17 files, +509/-99. Fixes Same-origin run preview sandbox lets untrusted preview JS use the viewer session #93 — new internal/preview/origin.go (isolated-origin validation), changes to internal/preview/{config,gateway,service}.go, internal/httpapi/run_preview.go, plus a new Chromium regression internal/httpapi/preview_isolation_browser_test.go and CI wiring (adds TestPreviewIsolationBrowser to platform.yml's REAL_BROWSER_TESTS, simplifies the Playwright install step, drops scripts/install-playwright-deps.sh).
  • Code review: isolatedOrigins()/parseOrigin()/sameHostname() correctly require an explicit, different-hostname AGENTFLOW_PREVIEW_FRAME_ANCESTORS (rejecting same host with a different port, wildcards, credentials, query strings, and case/trailing-dot variants). PublicURL() no longer derives scheme/host from X-Forwarded-Proto/request Host — it's fixed deployment config now, closing the original header-trust gap. gateway.go's ServeHTTP now 404s unless the request hits the configured preview hostname, and validateBackendOrigin is re-checked on every serve (not just at session creation), so previously-persisted sessions get revalidated too. run_preview.go's previewHost() middleware blocks the studio hostname's /preview* paths and only redirects stray navigation when already on the preview host. The rebuilt CSP (default-src 'none' + explicit per-resource directives + frame-ancestors built only from the validated parent list) matches the stated goal of also blocking credentialed no-cors asset requests to the studio.
  • Checked out the head SHA in an isolated worktree, diffed against base c87694a3. Ran go vet ./... (clean) and the full internal/preview + internal/httpapi suites: all pass, including the new table-driven TestPreviewRequiresIsolatedOrigin (11/11 subtests covering exactly the bypasses described above), TestPreviewOriginAndBackendBoundaries, and TestPreviewRevalidatesStoredBackend. Chatbot build + 299 vitest tests pass.
  • Missing evidence: the PR's own key regression, TestPreviewIsolationBrowser (real Chromium, signed viewer cookie, through the actual archive+gateway path), could not execute here — runtime.FindChromium() found no browser; after installing Playwright's Chromium manually it failed to launch (error while loading shared libraries: libnspr4.so), and this sandbox's apt-get cannot install the missing packages (privilege-drop syscalls seteuid/setgroups are denied even as root, and /var/cache/apt/archives/partial isn't writable) — this matches exactly the gap this PR's own deleted scripts/install-playwright-deps.sh comment described some runners not having. Separately, on GitHub's side this head SHA's platform/e2e/validate workflow runs are all completed/action_required, i.e. gated on a maintainer approving them to run.
  • To resolve: have a maintainer approve the pending platform/e2e/validate workflow runs on this SHA so TestPreviewIsolationBrowser actually executes in CI (which this PR itself wires in); or re-review in a sandbox with Chromium's system libraries available. All other local evidence (unit/table tests, go vet, chatbot build) is clean and the code review found no logic issues.

@AlexXPZhu local non-browser evidence is clean; the decisive TestPreviewIsolationBrowser just needs CI (or a properly provisioned sandbox) to run — could you or a maintainer approve the pending workflow runs on this SHA?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Same-origin run preview sandbox lets untrusted preview JS use the viewer session

3 participants