Repository navigation
Description
Summary
Run frontend preview is served on the studio origin at /preview/{sessionToken}/ and embedded in an iframe with sandbox tokens that include allow-scripts and allow-same-origin. Preview HTML/JS therefore shares the LoopForge origin with the logged-in studio. A viewer who opens Run → 前端预览 can have their authenticated session used for studio API actions via the parent window (and, when a preview backend is configured, via direct same-origin fetch because CSP connect-src then includes 'self').
HttpOnly on agentflow_user blocks document.cookie reads; it does not block credentialed same-origin requests initiated by script that shares the app origin.
Affected code (head c7114fca)
| Location | What the code does |
|---|---|
web/app/runs.js |
In-app preview iframe: src = session URL; sandbox="allow-scripts allow-forms allow-modals allow-downloads allow-same-origin" |
internal/preview/service.go |
PublicURL → {scheme}://{host}/preview/{token}/ on the app host (path hosting, not a separate preview origin) |
internal/preview/path.go |
HTTPPathPrefix = "/preview/" |
internal/preview/gateway.go |
Response CSP: sandbox … allow-same-origin; empty backend → connect-src 'none'; non-empty backend → connect-src 'self' <backendOrigin> |
internal/preview/service.go |
CreateSession mints opaque token; returns PublicURL |
internal/httpapi/run_preview.go |
POST /api/runs/:id/preview/session creates session for a ready preview |
internal/httpapi/server.go |
previewHost() is mounted before userAccessAuth() so /preview/{token}/ is served by opaque token, not browser login |
internal/httpapi/auth.go / #L322-L340 |
Cookie name agentflow_user, HttpOnly: true, Path: / |
internal/httpapi/space_artifacts.go |
Contrast: space HTML artifacts use CSP sandbox allow-scripts allow-forms without allow-same-origin |
docs/proposals/frontend-preview-delivery-lifecycle.md (§13.3) |
Design explicitly keeps path hosting + allow-same-origin for Vite ESM and documents the parent.fetch / studio-session cost |
Conditions (threat model — not anonymous Internet)
This is not “any anonymous visitor gets a session.” Entry requires content control plus a logged-in viewer opening the in-app preview.
- Content producer (attacker capability): someone who can cause a Run preview archive to contain attacker-influenced HTML/JS. Typical paths on this product:
- Project member who can enable preview on an SOP (
workflowSnapshot.preview.enabled/ sourcePath such asdist) and run it so the agent (or build step) writes static files that are later archived (archive_preview/ArchiveDir). - Anyone who can otherwise influence agent-built static output under the configured preview
sourcePathbefore finalization.
- Project member who can enable preview on an SOP (
- Victim: a signed-in studio user (cookie
agentflow_user) who opens Run → 前端预览 sopreviewPaneHTMLembeds/preview/{token}/in the same-origin iframe withallow-scripts+allow-same-origin. - Out of band for the parent-access path: “独立窗口” uses
window.open(url,'_blank','noopener')(runs.js~826) and is outside theparentaccess path. Directfetch('/api/…')from the preview document still depends on CSPconnect-srcwhen a backend is set.
Anonymous users without a session token cannot mint preview sessions through /api/… under normal auth-required deployments; possessing a leaked opaque preview URL loads preview content, but session-riding against studio APIs as described here targets a logged-in viewer of the in-app iframe.
Call chain (analytical — not executed in this audit)
- SOP with preview enabled completes; runner/engine archives
sourcePathinto a tar.zst preview bundle (HTML/JS as produced in the workspace). - Viewer calls
POST /api/runs/:id/preview/session; server returnsurl= same-host/preview/{token}/viaPublicURL. - Studio embeds that URL in an iframe with
allow-scripts+allow-same-origin(runs.js:738). - Preview document origin equals the LoopForge app origin (sandbox is not opaque).
- Parent path (always available for same-origin nested browsing with these sandbox flags): preview script can reach
window.parentand invoke studio APIs with the viewer’s cookies, e.g. conceptuallyparent.fetch('/api/…', {credentials:'include', …}). CSP on the preview response does not constrain the parent document’s network access. - Direct path (when preview backend URL is set):
previewCSPsetsconnect-src 'self' <backend>, so the preview document itself mayfetch('/api/…')with same-site cookies. When backend is empty,connect-srcis'none'(direct connect blocked); the parent path remains.
Not claimed here: a live browser capture, a successful token mint, or any packet capture. The above is inferred from static source and the project’s own design note at §13.3.
Impact
- Session-equivalent actions as the viewing user (create personal API tokens, read/modify data that user can access through studio APIs), without reading
document.cookie. - Cross-user impact inside a shared installation/project: one member’s crafted or agent-produced preview can target another member who only opens the preview tab.
- Design trade-off is acknowledged upstream (Vite ESM vs opaque origin); space-artifact CSP shows a stricter pattern already used elsewhere in-tree.
Suggested fix
- Prefer a dedicated preview origin (separate host) so
allow-same-origincannot equal the app origin. - Until then: never put the app API origin in preview
connect-srcas'self'; keep API on a different origin than preview files. - Treat framebust /
parentnullification as defense-in-depth only (bypassable); not the primary control. - Revisit whether Vite ESM can be served without same-origin sandbox (blob workers, opaque origin + absolute module map, or separate origin).
- Add a regression test that documents/fails closed on parent/API access attempts from preview active content.
Acceptance criteria
- Preview active content cannot perform credentialed calls to the studio API as the viewing user (separate origin and/or CSP that never allows app
'self'API connect from preview). - In-app iframe embedding cannot reach
window.parentapp privileges from preview script (separate origin, or equivalent isolation). - Threat model for agent-built preview bundles updated; automated regression covers the isolation property.
- Issue text and tests do not claim a browser PoC unless one is separately recorded.
Activity
fx-kurumi commentedon Nov 6, 2026
Additional minimal sandbox result: Local Chromium observed parent DOM/function access with the exact product sandbox (
allow-scripts,allow-same-origin); removingallow-same-originblocked both with an opaque origin. This was a synthetic parent/iframe check only; no LoopForge API, session, token, or real preview content was tested.AlexXPZhu commentedon Nov 11, 2026
I have submitted a fix in #187.
Archived previews now require a dedicated hostname and explicit studio frame origins. Preview tokens are not served on the studio host, platform routes are blocked on the preview host, and studio hostnames cannot be used as preview backends. Resource restrictions also cover image/script requests, while keeping ES modules and the existing History API shim working.
The PR includes a real Chromium regression with a signed viewer cookie, plus negative checks against the original production files. All local Go packages, frontend tests/build, vet and repository scans passed. Deployment configuration and compatibility changes are documented; no SQL migration or runner upgrade is required.
Robert0622 commentedon Nov 11, 2026
Triage update (follow-up on prior analysis)
Status recap from our side: we already implemented a phase-1 fix matching the issue's own proposed mitigation (close the
connect-src 'self'gap ininternal/preview/gateway.go'spreviewCSP, so the preview CSP never includes Studio's own origin, even when a preview backend is configured). Tests (go test ./internal/preview ./internal/httpapi) passed, but we have not been able to push/merge it yet due to an unrelated infra issue (our automation account currently lacks push permission on this repo -- being resolved on our side, not a code problem).Re: the note that a fix was submitted in #187 ("isolate archived previews from studio sessions", dedicated hostname + explicit studio frame origins) -- thanks for working on this. We reviewed #187: it's still open/unmerged as of now, so we can't confirm yet that it fully closes this out end-to-end. It looks complementary to our CSP-only phase-1 mitigation (addresses the
allow-same-origin+ shared-origin root cause more directly via a dedicated hostname), but the original issue's second concern --window.parentaccess, which CSP alone cannot block -- still needs the origin-isolation approach #187 is pursuing.Conclusion: confirmed valid security issue, not yet fully resolved. Recommend reviewing/merging #187 for the origin-isolation piece; we'll reconcile our CSP-only patch with it once push access is restored. No behavior-change claims until #187 lands and is verified against the acceptance criteria in the original report (no viewer-session API calls from preview content via
fetchorwindow.parent).