diff --git a/packages/core/api/src/oauth-popup.test.ts b/packages/core/api/src/oauth-popup.test.ts index 31fbacb36..12efa6971 100644 --- a/packages/core/api/src/oauth-popup.test.ts +++ b/packages/core/api/src/oauth-popup.test.ts @@ -115,9 +115,11 @@ describe("popupDocument", () => { expect(html).not.toContain(" { - const html = popupDocument(successPayload, 'evil"name'); - expect(html).toContain('new BroadcastChannel("evil"name")'); + it("serializes the channel name as JavaScript so the exact channel is preserved", () => { + const html = popupDocument(successPayload, 'evil"name\\path'); + expect(html).toContain('new BroadcastChannel("evil\\"name\\\\path")'); + expect(html).toContain('localStorage.setItem("evil\\"name\\\\path",JSON.stringify(p))'); + expect(html).not.toContain("evil"name"); }); it("escapes < > & in the serialized script payload to prevent breakout", () => { @@ -138,6 +140,15 @@ describe("popupDocument", () => { expect(scriptLiteral).toContain("\\u003c/script\\u003e"); }); + it("escapes < > & in the serialized channel name to prevent breakout", () => { + const html = popupDocument(successPayload, 'channel'); + const scriptMatch = /"); + expect(script).toContain("channel\\u003c/script\\u003e"); + }); + it("posts to window.opener AND falls back to BroadcastChannel with the given channel name", () => { const html = popupDocument(successPayload, "executor:openapi-oauth-result"); expect(html).toContain("window.opener.postMessage(p,window.location.origin)"); diff --git a/packages/core/api/src/oauth-popup.ts b/packages/core/api/src/oauth-popup.ts index 9af0d0890..adedc36b4 100644 --- a/packages/core/api/src/oauth-popup.ts +++ b/packages/core/api/src/oauth-popup.ts @@ -91,7 +91,7 @@ export const popupDocument = ( const icon = payload.ok ? '' : ''; - const escapedChannel = escapeHtml(channelName); + const serializedChannel = serializeForScript(channelName); const detailsHtml = details ? `
Details
${escapeHtml(details)}
` : ""; @@ -117,8 +117,8 @@ ${detailsHtml} // raced by the auto-close — so localStorage (a 'storage' event on the opener) is // the reliable fallback. The opener settles on whichever lands first. try{if(window.opener)window.opener.postMessage(p,window.location.origin)}catch(e){} -try{if("BroadcastChannel"in window){const c=new BroadcastChannel("${escapedChannel}");c.postMessage(p);setTimeout(()=>c.close(),100)}}catch(e){} -try{localStorage.setItem("${escapedChannel}",JSON.stringify(p))}catch(e){} +try{if("BroadcastChannel"in window){const c=new BroadcastChannel(${serializedChannel});c.postMessage(p);setTimeout(()=>c.close(),100)}}catch(e){} +try{localStorage.setItem(${serializedChannel},JSON.stringify(p))}catch(e){} if(p.ok)setTimeout(()=>window.close(),400);})(); `;