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:
@@ -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,
|
||||
|
||||
@@ -17,6 +17,7 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe
|
||||
translations: Record<string, string>;
|
||||
ciphers: InlineMenuCipherData[] | null;
|
||||
portName: string;
|
||||
extensionOrigin?: string;
|
||||
};
|
||||
|
||||
export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage &
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user