1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-28 22:23:28 +00:00

[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
This commit is contained in:
Daniel Riera
2025-11-21 12:10:03 -05:00
committed by GitHub
parent 23d566685e
commit 279632d65f
4 changed files with 61 additions and 14 deletions

View File

@@ -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,

View File

@@ -17,6 +17,7 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe
translations: Record<string, string>;
ciphers: InlineMenuCipherData[] | null;
portName: string;
extensionOrigin?: string;
};
export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage &

View File

@@ -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);
});
});
});

View File

@@ -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;
}