From 571bf7ebcf3af25ac83ed9fa098be9126521f7f9 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Tue, 28 Jul 2026 17:16:46 +0200 Subject: [PATCH] fix(apps): hand the sandboxed SDK token over only once per loaded document Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz --- .../apps/editor/AppEditorHeaderDeploy.svelte | 13 ++++------ .../apps/editor/PublicAppFrame.svelte | 6 ++--- .../components/raw_apps/RawAppPreview.svelte | 24 +++++++++++++++---- .../src/lib/components/raw_apps/sdkScopes.ts | 6 ----- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte index 2df3c83354..722d86f2d8 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -21,10 +21,7 @@ type OnBehalfOfChoice } from '$lib/components/OnBehalfOfSelector.svelte' import { canUserBypassRuleKind, protectionRulesState } from '$lib/workspaceProtectionRules.svelte' - import { - FRONTEND_SDK_SCOPES, - MIN_SANDBOXED_SDK_VERSION - } from '$lib/components/raw_apps/sdkScopes' + import { FRONTEND_SDK_SCOPES } from '$lib/components/raw_apps/sdkScopes' const WM_DEPLOYERS_GROUP = 'wm_deployers' @@ -373,10 +370,10 @@ {/if} {#if policy.sandbox == true && policy.frontend_sdk_scopes?.length}
- - A sandboxed app calls the API cross-origin, which earlier versions of the SDK cannot do. - Make sure your app depends on windmill-client@^{MIN_SANDBOXED_SDK_VERSION} and - redeploy — apps bundled against an older version fail with a CORS error. + + A sandboxed app calls the API cross-origin, which older windmill-client versions + cannot do. An app bundled before this Windmill version fails with a CORS error until you deploy + it again, which re-bundles it against a current client.
{/if} diff --git a/frontend/src/lib/components/apps/editor/PublicAppFrame.svelte b/frontend/src/lib/components/apps/editor/PublicAppFrame.svelte index a62c640667..3ab9ea5bc7 100644 --- a/frontend/src/lib/components/apps/editor/PublicAppFrame.svelte +++ b/frontend/src/lib/components/apps/editor/PublicAppFrame.svelte @@ -241,7 +241,7 @@ }) // Read by RawAppPreview: the consented viewer-scoped SDK token to expose to - // the bundle (unsandboxed raw apps only). + // the bundle, sandboxed or not. setContext('RAW_APP_SDK_TOKEN', { get value() { return sdkToken @@ -268,8 +268,8 @@ appPath = resp.app_path ?? undefined workspaceId = resp.workspace_id ?? undefined // The backend only advertises scopes where a token could actually be - // minted (raw, unsandboxed, viewer authenticated), and mints one only on - // the follow-up request carrying the viewer's consent. + // minted (a raw app, viewer authenticated), and mints one only on the + // follow-up request carrying the viewer's consent. sdkScopes = resp.sdk_scopes?.length ? resp.sdk_scopes : undefined viewerEmail = resp.viewer_email ?? '' sdkToken = undefined diff --git a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte index 8aac3ca5de..2b07728d07 100644 --- a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte @@ -57,10 +57,11 @@ // - DEFAULT (isolated): a real API URL serving a sandboxed, opaque-origin // document (`CSP: sandbox` response header + the iframe sandbox attribute), // so a malicious bundle can never reach the authenticated Windmill origin - // (no cookie, no window.parent, no token). Root-relative so it resolves - // against the real host even when this component itself runs inside an opaque - // viewer (where `location.origin` is "null"). Context is handed over via - // postMessage — never baked into the document, never a credential. + // (no cookie, no window.parent). Root-relative so it resolves against the + // real host even when this component itself runs inside an opaque viewer + // (where `location.origin` is "null"). Context — and, when the viewer + // approved SDK scopes, the viewer-scoped token — is handed over via + // postMessage, never baked into the document. // - UNSANDBOXED (the default — publisher did not opt into isolation): a // client-built blob: wrapper (same-origin with the SPA) loaded with `allow-same-origin`, // so relative `fetch('/api/...')` and the session cookie work. The backend @@ -150,8 +151,21 @@ } catch (_) {} } + // The token may only be handed over once per document we loaded ourselves. The + // reply is addressed to a browsing context, not a document, and it must target + // `*` because the sandboxed frame has an opaque origin — so a page the bundle + // (or a link the viewer clicked) navigated to keeps the same `contentWindow` + // and could ask for the credential again by posting `windmill:ready`. Reset + // only when WE change the iframe's src, which a navigation away does not. + let sdkHandedOff = false + $effect(() => { + iframeSrc + sdkHandedOff = false + }) + function respondCtx() { - const sdkToken = sdkTokenCtx?.value + const sdkToken = sdkHandedOff ? undefined : sdkTokenCtx?.value + if (sdkToken) sdkHandedOff = true iframe?.contentWindow?.postMessage( { type: 'windmill:ctx', diff --git a/frontend/src/lib/components/raw_apps/sdkScopes.ts b/frontend/src/lib/components/raw_apps/sdkScopes.ts index e31b7e6515..9a825fc02b 100644 --- a/frontend/src/lib/components/raw_apps/sdkScopes.ts +++ b/frontend/src/lib/components/raw_apps/sdkScopes.ts @@ -3,12 +3,6 @@ // in the backend `apps.rs` — both lists must stay in sync), plus the viewer-side // consent persistence for the permission banner. -/** A sandboxed app calls the API from an opaque origin, which needs the - * credential-free client shipped alongside this release: earlier versions force - * `credentials: 'include'`, which CORS rejects. Tracks the monorepo version - * because `windmill-client` is published from it in lockstep. */ -export const MIN_SANDBOXED_SDK_VERSION = __pkg__.version - export const FRONTEND_SDK_SCOPES: { value: string; label: string; description: string }[] = [ { value: 'jobs:run',