From 279632d65f1e8dfe1af0a1d2fdce5c11bd5637c1 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 21 Nov 2025 12:10:03 -0500 Subject: [PATCH] [PM-28516] Inline menu is not working in main (#17524) * PM-28516 alidate iframe and stylesheet URLs against their own origins to handle cases where chrome assigns different extension ids in different contexts * switch to regex to match exisiting match pattern * updated regex to account for safari --- .../autofill/background/overlay.background.ts | 17 ++++++---- .../autofill-inline-menu-container.ts | 1 + .../autofill-inline-menu-container.spec.ts | 34 +++++++++++++++++++ .../autofill-inline-menu-container.ts | 23 ++++++++----- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 225cbbe66ca..0eb7d070de3 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -2949,17 +2949,21 @@ export class OverlayBackground implements OverlayBackgroundInterface { (await this.checkFocusedFieldHasValue(port.sender.tab)) && (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)); + const iframeUrl = chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, + ); + const styleSheetUrl = chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, + ); + const extensionOrigin = new URL(iframeUrl).origin; + this.postMessageToPort(port, { command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, - iframeUrl: chrome.runtime.getURL( - `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, - ), + iframeUrl, pageTitle: chrome.i18n.getMessage( isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton", ), - styleSheetUrl: chrome.runtime.getURL( - `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, - ), + styleSheetUrl, theme: await firstValueFrom(this.themeStateService.selectedTheme$), translations: this.getInlineMenuTranslations(), ciphers: isInlineMenuListPort ? await this.getInlineMenuCipherData() : null, @@ -2973,6 +2977,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { showSaveLoginMenu, showInlineMenuAccountCreation, authStatus, + extensionOrigin, }); this.updateInlineMenuPosition( port.sender, diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts index af60d1de77d..64fa8dde124 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts @@ -17,6 +17,7 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe translations: Record; ciphers: InlineMenuCipherData[] | null; portName: string; + extensionOrigin?: string; }; export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage & diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts index d7a61bec61f..e0a6e626b3c 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts @@ -184,4 +184,38 @@ describe("AutofillInlineMenuContainer", () => { expect(port.postMessage).not.toHaveBeenCalled(); }); }); + + describe("isExtensionUrlWithOrigin", () => { + it("validates extension URLs with matching origin", () => { + const url = "chrome-extension://test-id/path/to/file.html"; + const origin = "chrome-extension://test-id"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(true); + }); + + it("rejects extension URLs with mismatched origin", () => { + const url = "chrome-extension://test-id/path/to/file.html"; + const origin = "chrome-extension://different-id"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(false); + }); + + it("validates extension URL against its own origin when no expectedOrigin provided", () => { + const url = "moz-extension://test-id/path/to/file.html"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url)).toBe(true); + }); + + it("rejects non-extension protocols", () => { + const url = "https://example.com/path"; + const origin = "https://example.com"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(false); + }); + + it("rejects empty or invalid URLs", () => { + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"]("")).toBe(false); + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"]("not-a-url")).toBe(false); + }); + }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index ad0b11f0bc6..aea6ef30b99 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -87,11 +87,13 @@ export class AutofillInlineMenuContainer { return; } - if (!this.isExtensionUrl(message.iframeUrl)) { + const expectedOrigin = message.extensionOrigin || this.extensionOrigin; + + if (!this.isExtensionUrlWithOrigin(message.iframeUrl, expectedOrigin)) { return; } - if (message.styleSheetUrl && !this.isExtensionUrl(message.styleSheetUrl)) { + if (message.styleSheetUrl && !this.isExtensionUrlWithOrigin(message.styleSheetUrl)) { return; } @@ -115,20 +117,25 @@ export class AutofillInlineMenuContainer { } /** - * validates that a URL is from the extension origin. - * prevents loading arbitrary URLs in the iframe. + * Validates that a URL uses an extension protocol and matches the expected extension origin. + * If no expectedOrigin is provided, validates against the URL's own origin. * * @param url - The URL to validate. */ - private isExtensionUrl(url: string): boolean { + private isExtensionUrlWithOrigin(url: string, expectedOrigin?: string): boolean { if (!url) { return false; } try { const urlObj = new URL(url); - return ( - urlObj.origin === this.extensionOrigin || urlObj.href.startsWith(this.extensionOrigin + "/") - ); + const isExtensionProtocol = /^[a-z]+(-[a-z]+)?-extension:$/i.test(urlObj.protocol); + + if (!isExtensionProtocol) { + return false; + } + + const originToValidate = expectedOrigin ?? urlObj.origin; + return urlObj.origin === originToValidate || urlObj.href.startsWith(originToValidate + "/"); } catch { return false; }