diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index cfbb8a8e2a..a272aa85e0 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -826,6 +826,10 @@ fn raw_app_wrapper_html(secret: &str) -> String { document.body.appendChild(s); } window.addEventListener('message', function (e) { + // Only our embedder may supply the context. It now carries the SDK's token + // and API base, so accepting it from any window would let another frame + // point the bundle's API calls at a host it controls. + if (e.source !== window.parent) return; var d = e.data || {}; if (d.type === 'windmill:ctx') { window.ctx = d.ctx; diff --git a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte index 6b1255ac91..0ba53f5433 100644 --- a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte @@ -4,10 +4,11 @@ import type { Runnable } from './rawAppPolicy' import { getContext, onMount, untrack } from 'svelte' import { unsandboxedRawAppHtml } from './utils' - import { randomUUID } from '$lib/utils/uuid' + import { randomSecret } from '$lib/utils/uuid' // Per-mount secret proving a `windmill:ready` came from the document we loaded. - const handshakeNonce = randomUUID() + // Gates a credential, so it must be unguessable — not `randomUUID`. + const handshakeNonce = randomSecret() interface Props { workspace: string diff --git a/frontend/src/lib/utils/uuid.ts b/frontend/src/lib/utils/uuid.ts index 4a9f092dac..fbdb54dd7e 100644 --- a/frontend/src/lib/utils/uuid.ts +++ b/frontend/src/lib/utils/uuid.ts @@ -5,3 +5,13 @@ export function randomUUID() { return v.toString(16) }) } + +/** Unguessable random token, for values whose unpredictability is load-bearing — + * anything gating a credential. `randomUUID` above is `Math.random()`-based and + * must not be used for those. `getRandomValues` (unlike `crypto.randomUUID`) is + * also available in insecure contexts, so this needs no fallback. */ +export function randomSecret(bytes = 32): string { + const buf = new Uint8Array(bytes) + crypto.getRandomValues(buf) + return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('') +} diff --git a/typescript-client/client.ts b/typescript-client/client.ts index d52421b3e8..178ade222f 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -68,10 +68,11 @@ export function setClient(token?: string, baseUrl?: string) { if (token === undefined) { token = getEnv("WM_TOKEN") ?? "no_token"; } - // Cookies are the fallback for when there is no bearer token. Sending both is - // not just redundant, it breaks the browser case outright: the API answers - // `Access-Control-Allow-Origin: *` and so can never allow credentials, and a + // Credentials must be off whenever a bearer token is in play: the API answers + // `Access-Control-Allow-Origin: *` and so can never allow them, and a // credentialed cross-origin request is rejected before the token is looked at. + // (`"no_token"` is still sent as a bearer, so this branch is about credentials + // mode only — it does not make cookie auth work.) OpenAPI.WITH_CREDENTIALS = token === "no_token"; OpenAPI.TOKEN = token; OpenAPI.BASE = baseUrl + "/api";