From 4e94731e432eed7b4281447c9ba3de020419e0d8 Mon Sep 17 00:00:00 2001 From: Erik Johansen Date: Tue, 29 Sep 2026 09:06:47 +0200 Subject: [PATCH] enforce origin checks between editor and content iframe The iframe logged disallowed origins but still accepted their editor port, so any site framing a page could drive the content editor. The origin check also used unanchored matching, so a string like "http://localhost:3000" matched "http://localhost:3000.evil.com". - Ignore messages that are not a Yancy.init request with a port - Stop handling messages from origins that are not allowed - Compare string origins exactly, and require RegExps to match the whole origin - Default allowOrigins to the page's own origin, and set it even when window.Yancy already exists - Ignore editor commands that are not editor chain methods - Send the editor port only to the site origin instead of "*", so a page the iframe navigated to can not receive it Co-Authored-By: Claude Opus 5.5 --- lib/Yancy/Editor/src/content-editor.svelte | 9 ++++-- lib/Yancy/Editor/src/iframe.ts | 36 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/Yancy/Editor/src/content-editor.svelte b/lib/Yancy/Editor/src/content-editor.svelte index 75e7dad1..b2e05208 100644 --- a/lib/Yancy/Editor/src/content-editor.svelte +++ b/lib/Yancy/Editor/src/content-editor.svelte @@ -63,9 +63,14 @@ console.debug("editor iframe loaded", e); channel = new MessageChannel(); channel.port1.onmessage = onmessage; - // Initialize the editor interface by sending the page a MessagePort to use + // Initialize the editor interface by sending the page a MessagePort to use. + // Only send it to the site origin, so pages the iframe navigated away to + // can not get it. if (e.target instanceof HTMLIFrameElement && e.target.contentWindow) { - e.target.contentWindow.postMessage("Yancy.init", "*", [channel.port2]); + const origin = new URL(e.target.src, window.location.href).origin; + e.target.contentWindow.postMessage("Yancy.init", origin, [ + channel.port2, + ]); } }; diff --git a/lib/Yancy/Editor/src/iframe.ts b/lib/Yancy/Editor/src/iframe.ts index cd5cd9f7..2c5ebe0b 100644 --- a/lib/Yancy/Editor/src/iframe.ts +++ b/lib/Yancy/Editor/src/iframe.ts @@ -4,9 +4,23 @@ import ContentField from "./content-field.svelte"; import { mount } from "svelte"; import "./iframe.css"; -const Yancy = (window.Yancy ??= { - allowOrigins: ["http://localhost:3000"], -}); +const Yancy = (window.Yancy ??= {}); +// By default, only allow an editor on the same origin as this page +Yancy.allowOrigins ??= [window.location.origin]; + +/** + * Check if the given origin is allowed to control this page. Strings must + * match the origin exactly, and RegExps must match the entire origin. + */ +function isAllowedOrigin(origin: string): boolean { + return (Yancy.allowOrigins ?? []).some((o) => { + if (typeof o === "string") { + return o === origin; + } + const m = origin.match(o); + return m !== null && m.index === 0 && m[0] === origin; + }); +} type YancyEditorMessage = { name: string; @@ -86,6 +100,10 @@ function handleEvent(e: MessageEvent) { let chain = getFocusedEditor().chain(); for (const c of commandEvent.command) { const [method, ...args] = typeof c === "string" ? [c] : c; + if (typeof chain[method] !== "function") { + console.error(`unknown editor command ${method}`); + return; + } chain = chain[method](...args); } chain.run(); @@ -94,13 +112,21 @@ function handleEvent(e: MessageEvent) { window.addEventListener("message", (e: MessageEvent) => { console.debug("got window message", e); - if (!Yancy.allowOrigins.find((o) => e.origin.match(o))) { + // Only handle Yancy editor initialization requests + if ( + typeof e.data !== "string" || + !e.data.startsWith("Yancy.init") || + !e.ports[0] + ) { + return; + } + if (!isAllowedOrigin(e.origin)) { console.error(`origin ${e.origin} not allowed.`, { allowOrigins: Yancy.allowOrigins, }); + return; } - // Handle Yancy editor initialization request const [, , version] = e.data.split(/:/); if (version < 0) { console.error("Yancy editor version not supported by iframe", { version });