fix(apps): use an unguessable nonce for the sandboxed SDK handoff

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
This commit is contained in:
Diego Imbert
2026-07-28 17:56:04 +02:00
co-authored by Claude Fable 5
parent 4f4db90bf3
commit 9069eccb60
4 changed files with 21 additions and 5 deletions
+4
View File
@@ -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;
@@ -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
+10
View File
@@ -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('')
}
+4 -3
View File
@@ -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";