From fcbbeaf979e0ce86a4cbe257bfe75ea0ecd6e4fa Mon Sep 17 00:00:00 2001 From: user Date: Fri, 28 Aug 2026 19:02:22 +0800 Subject: [PATCH] fix(web-ui): verify message source before applying widget updates The generative widget iframe shell listened for `message` events without checking where they came from. Any window able to postMessage into the host page could forge a `bitfun-widget:update` event and have arbitrary HTML applied inside the widget via setContent. Reject every message whose source is not the direct parent window before any data processing, mirroring the source check already applied to the iframe-to-parent direction. Cover the guard with an anti-injection test asserting the gate sits ahead of setContent in the widget shell. Test: pnpm --dir src/web-ui run type-check; pnpm --dir src/web-ui run lint; vitest run GenerativeWidgetFrame.test.tsx (3 passed). AI: AI-assisted, locally tested (type-check + lint + targeted vitest). --- .../GenerativeWidgetFrame.test.tsx | 17 +++++++++++++++++ .../generative-widget/GenerativeWidgetFrame.tsx | 1 + 2 files changed, 18 insertions(+) diff --git a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.test.tsx b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.test.tsx index 04da33f4ec..3584d2bf81 100644 --- a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.test.tsx +++ b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.test.tsx @@ -63,4 +63,21 @@ describe('GenerativeWidgetFrame shell', () => { expect(iframe.getAttribute('sandbox')).toContain('allow-same-origin'); expect(iframe.contentDocument?.documentElement.outerHTML).toContain('bitfun-widget'); }); + + it('rejects non-parent-window message sources before processing widget updates (anti-injection)', () => { + // The widget shell listens for `bitfun-widget:update` posted by its host page. + // Any other window able to postMessage into the host could forge that event + // and have arbitrary HTML applied via setContent, so the source gate must + // reject non-parent windows before any data processing (including the + // setContent call) happens. + const shellSource = GENERATIVE_WIDGET_SHELL_HTML; + const listenerStart = shellSource.indexOf("window.addEventListener('message'"); + const setContentCall = shellSource.indexOf('setContent(', listenerStart); + + expect(listenerStart).toBeGreaterThan(-1); + expect(setContentCall).toBeGreaterThan(listenerStart); + + const listenerBody = shellSource.slice(listenerStart, setContentCall); + expect(listenerBody).toMatch(/if \(event\.source !== window\.parent\) return;/); + }); }); diff --git a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx index 30c0839cad..8395ffc95c 100644 --- a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx +++ b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx @@ -860,6 +860,7 @@ ${createWidgetAppearanceStaticShellCss()} }, true); window.addEventListener('message', function (event) { + if (event.source !== window.parent) return; var data = event.data; if (!data) return; if (data.type === 'bitfun-widget:clear-selection') {