From af4656eb2b366207203c29aca374909a7c97c769 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Sat, 23 May 2026 07:42:09 -0700 Subject: [PATCH] fix(trace-viewer): allow opt-in postMessage origin via search param The same-origin postMessage check added in #40548 closed off a legitimate integration used by third-party HTML report systems where the trace viewer iframe is hosted on a different origin than the report shell. Restore those flows through an explicit ?allowPostMessageOrigin= query param while keeping the strict same-origin default for everyone else. The opt-in is normalized through the URL parser so callers cannot smuggle in paths or wildcards, and a single origin is supported per load. Fixes #40960. --- .../trace-viewer/src/ui/workbenchLoader.tsx | 13 +++- tests/library/trace-viewer.spec.ts | 65 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 5a112d23775a3..eddcbb8fd6bc7 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -68,8 +68,19 @@ export const WorkbenchLoader: React.FunctionComponent<{ return () => document.removeEventListener('paste', listener); }); React.useEffect(() => { + const allowedOrigins = new Set([window.location.origin]); + const allowParam = new URL(window.location.href).searchParams.get('allowPostMessageOrigin'); + if (allowParam) { + try { + // Normalize via the URL parser so callers cannot smuggle in paths or + // wildcards, and so we compare on the same shape MessageEvent.origin uses. + allowedOrigins.add(new URL(allowParam).origin); + } catch { + // Ignore malformed values; default same-origin policy still applies. + } + } const listener = (e: MessageEvent) => { - if (e.origin !== window.location.origin) + if (!allowedOrigins.has(e.origin)) return; const { method, params } = e.data; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index eb2008452f5b0..3e5b8faef5e6c 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -2062,6 +2062,71 @@ test("shouldn't render not-blob trace received from message", async ({ showTrace await expect(traceViewer.actionTitles).not.toBeVisible(); }); +test('should ignore cross-origin blob trace by default', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40960' }, +}, async ({ showTraceViewer, server }) => { + const traceViewer = await showTraceViewer(undefined, { host: 'localhost' }); + const viewerURL = traceViewer.page.url(); + + // Host an embedder page on a different origin than the viewer and have it + // iframe the viewer, then push a Blob to it via postMessage. Without an + // explicit opt-in the viewer must ignore cross-origin messages. + server.setRoute('/embed-trace-viewer.html', (_, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); + await traceViewer.page.goto(`${server.PREFIX}/embed-trace-viewer.html`); + await traceViewer.page.frameLocator('#viewer').locator('.drop-target').waitFor(); + + await traceViewer.page.evaluate(trace => { + const uint8Array = Uint8Array.from(atob(trace), c => c.charCodeAt(0)); + const iframe = document.getElementById('viewer') as HTMLIFrameElement; + iframe.contentWindow!.postMessage({ + method: 'load', + params: { + trace: new Blob([uint8Array], { type: 'application/zip' }), + } + }, '*'); + }, fs.readFileSync(traceFile, 'base64')); + + // Drop target should remain visible inside the iframe, meaning the load was rejected. + const viewerFrame = traceViewer.page.frameLocator('#viewer'); + // Give the listener a chance to run before asserting nothing happened. + await traceViewer.page.waitForTimeout(500); + await expect(viewerFrame.locator('.drop-target')).toBeVisible(); + await expect(viewerFrame.locator('.action-title')).not.toBeVisible(); +}); + +test('should accept cross-origin blob trace when allowPostMessageOrigin matches', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/40960' }, +}, async ({ showTraceViewer, server }) => { + const traceViewer = await showTraceViewer(undefined, { host: 'localhost' }); + const viewerURL = new URL(traceViewer.page.url()); + viewerURL.searchParams.set('allowPostMessageOrigin', server.PREFIX); + + server.setRoute('/embed-trace-viewer.html', (_, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); + await traceViewer.page.goto(`${server.PREFIX}/embed-trace-viewer.html`); + const viewerFrame = traceViewer.page.frameLocator('#viewer'); + await viewerFrame.locator('.drop-target').waitFor(); + + await traceViewer.page.evaluate(trace => { + const uint8Array = Uint8Array.from(atob(trace), c => c.charCodeAt(0)); + const iframe = document.getElementById('viewer') as HTMLIFrameElement; + iframe.contentWindow!.postMessage({ + method: 'load', + params: { + trace: new Blob([uint8Array], { type: 'application/zip' }), + } + }, '*'); + }, fs.readFileSync(traceFile, 'base64')); + + await expect(viewerFrame.locator('.drop-target')).not.toBeVisible(); + await expect(viewerFrame.locator('.action-title').first()).toBeVisible(); +}); + test('should not trip over complex urls in style tags', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/35681' }, }, async ({ runAndTrace, page }) => {