1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-24 02:41:54 +01:00

[PM-5189] Implementing the message-connector to ensure we are sending messages without posting a window message

This commit is contained in:
Cesar Gonzalez 2024-04-07 20:35:51 -05:00
parent 3313807acb
commit d6d52e5e5e
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
20 changed files with 182 additions and 91 deletions

View File

@ -12,10 +12,10 @@ type PageDetailsForTab = Record<
>; >;
type SubFrameOffsetData = { type SubFrameOffsetData = {
frameId?: number;
url?: string; url?: string;
top: number; top: number;
left: number; left: number;
frameId?: number;
} | null; } | null;
type SubFrameOffsetsForTab = Record< type SubFrameOffsetsForTab = Record<
@ -49,9 +49,9 @@ type OverlayBackgroundExtensionMessage = {
forceCloseOverlay?: boolean; forceCloseOverlay?: boolean;
isOverlayHidden?: boolean; isOverlayHidden?: boolean;
setTransparentOverlay?: boolean; setTransparentOverlay?: boolean;
data?: LockedVaultPendingNotificationsData;
isFieldCurrentlyFocused?: boolean; isFieldCurrentlyFocused?: boolean;
isCurrentlyFilling?: boolean; isCurrentlyFilling?: boolean;
data?: LockedVaultPendingNotificationsData;
} & OverlayAddNewItemMessage; } & OverlayAddNewItemMessage;
type OverlayPortMessage = { type OverlayPortMessage = {
@ -132,6 +132,7 @@ type OverlayButtonPortMessageHandlers = {
forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void;
overlayPageBlurred: () => void; overlayPageBlurred: () => void;
redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
getPageColorScheme: () => void;
}; };
type OverlayListPortMessageHandlers = { type OverlayListPortMessageHandlers = {
@ -144,6 +145,7 @@ type OverlayListPortMessageHandlers = {
addNewVaultItem: ({ port }: PortConnectionParam) => void; addNewVaultItem: ({ port }: PortConnectionParam) => void;
viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void;
redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void;
updateAutofillOverlayListHeight: ({ message, port }: PortOnMessageHandlerParams) => void;
}; };
interface OverlayBackground { interface OverlayBackground {

View File

@ -95,6 +95,11 @@ class OverlayBackground implements OverlayBackgroundInterface {
this.closeOverlay(port.sender, { forceCloseOverlay: true }), this.closeOverlay(port.sender, { forceCloseOverlay: true }),
overlayPageBlurred: () => this.checkOverlayListFocused(), overlayPageBlurred: () => this.checkOverlayListFocused(),
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
getPageColorScheme: () => {
this.overlayButtonPort?.postMessage({
command: "getPageColorScheme",
});
},
}; };
private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = {
checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(),
@ -106,6 +111,12 @@ class OverlayBackground implements OverlayBackgroundInterface {
addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port),
viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port),
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
updateAutofillOverlayListHeight: ({ message }) => {
this.overlayListPort?.postMessage({
command: "updateIframePosition",
styles: message.styles,
});
},
}; };
constructor( constructor(
@ -832,7 +843,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
} }
private updateIsFieldCurrentlyFocused({ message }: OverlayBackgroundExtensionMessage) { private updateIsFieldCurrentlyFocused(message: OverlayBackgroundExtensionMessage) {
this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused; this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused;
} }
@ -910,6 +921,14 @@ class OverlayBackground implements OverlayBackgroundInterface {
* @param port - The port that connected to the extension background * @param port - The port that connected to the extension background
*/ */
private handlePortOnConnect = async (port: chrome.runtime.Port) => { private handlePortOnConnect = async (port: chrome.runtime.Port) => {
const isOverlayListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector;
const isOverlayButtonMessageConnector =
port.name === AutofillOverlayPort.ButtonMessageConnector;
if (isOverlayListMessageConnector || isOverlayButtonMessageConnector) {
port.onMessage.addListener(this.handleOverlayElementPortMessage);
return;
}
const isOverlayListPort = port.name === AutofillOverlayPort.List; const isOverlayListPort = port.name === AutofillOverlayPort.List;
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
@ -923,7 +942,6 @@ class OverlayBackground implements OverlayBackgroundInterface {
this.overlayButtonPort = port; this.overlayButtonPort = port;
} }
port.onMessage.addListener(this.handleOverlayElementPortMessage);
port.onDisconnect.addListener(this.handlePortOnDisconnect); port.onDisconnect.addListener(this.handlePortOnDisconnect);
port.postMessage({ port.postMessage({
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
@ -932,6 +950,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
theme: await firstValueFrom(this.themeStateService.selectedTheme$), theme: await firstValueFrom(this.themeStateService.selectedTheme$),
translations: this.getTranslations(), translations: this.getTranslations(),
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
messageConnectorUrl: chrome.runtime.getURL("overlay/message-connector.html"),
}); });
void this.updateOverlayPosition( void this.updateOverlayPosition(
{ {
@ -956,11 +975,11 @@ class OverlayBackground implements OverlayBackgroundInterface {
const command = message?.command; const command = message?.command;
let handler: CallableFunction | undefined; let handler: CallableFunction | undefined;
if (port.name === AutofillOverlayPort.Button) { if (port.name === AutofillOverlayPort.ButtonMessageConnector) {
handler = this.overlayButtonPortMessageHandlers[command]; handler = this.overlayButtonPortMessageHandlers[command];
} }
if (port.name === AutofillOverlayPort.List) { if (port.name === AutofillOverlayPort.ListMessageConnector) {
handler = this.overlayListPortMessageHandlers[command]; handler = this.overlayListPortMessageHandlers[command];
} }

View File

@ -24,7 +24,7 @@ const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers
globalThis.addEventListener("load", load); globalThis.addEventListener("load", load);
function load() { function load() {
setupWindowMessageListener(); setupWindowMessageListener();
postMessageToParent({ command: "initNotificationBar" }); postMessageToConnector({ command: "initNotificationBar" });
} }
function initNotificationBar(message: NotificationBarWindowMessage) { function initNotificationBar(message: NotificationBarWindowMessage) {
@ -392,6 +392,6 @@ function setNotificationBarTheme() {
document.documentElement.classList.add(`theme_${theme}`); document.documentElement.classList.add(`theme_${theme}`);
} }
function postMessageToParent(message: NotificationBarWindowMessage) { function postMessageToConnector(message: NotificationBarWindowMessage) {
globalThis.parent.postMessage(message, windowMessageOrigin || "*"); globalThis.parent.postMessage(message, windowMessageOrigin || "*");
} }

View File

@ -7,6 +7,7 @@ type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: Authenticati
type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & { type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & {
styleSheetUrl: string; styleSheetUrl: string;
translations: Record<string, string>; translations: Record<string, string>;
messageConnectorUrl: string;
}; };
type OverlayButtonWindowMessageHandlers = { type OverlayButtonWindowMessageHandlers = {

View File

@ -7,7 +7,6 @@ type AutofillOverlayIframeExtensionMessage = {
type AutofillOverlayIframeWindowMessageHandlers = { type AutofillOverlayIframeWindowMessageHandlers = {
[key: string]: CallableFunction; [key: string]: CallableFunction;
updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void; updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void;
getPageColorScheme: () => void;
}; };
type AutofillOverlayIframeExtensionMessageParam = { type AutofillOverlayIframeExtensionMessageParam = {
@ -19,6 +18,7 @@ type BackgroundPortMessageHandlers = {
initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void;
getPageColorScheme: () => void;
}; };
interface AutofillOverlayIframeService { interface AutofillOverlayIframeService {

View File

@ -14,6 +14,7 @@ type InitAutofillOverlayListMessage = OverlayListMessage & {
theme: string; theme: string;
translations: Record<string, string>; translations: Record<string, string>;
ciphers?: OverlayCipherData[]; ciphers?: OverlayCipherData[];
messageConnectorUrl: string;
}; };
type OverlayListWindowMessageHandlers = { type OverlayListWindowMessageHandlers = {

View File

@ -70,13 +70,13 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
* unobserve the body element to ensure the mutation observer no * unobserve the body element to ensure the mutation observer no
* longer triggers. * longer triggers.
*/ */
private removeInlineMenu = (message: any) => { private removeInlineMenu = (message?: AutofillExtensionMessage) => {
if (message.overlayElement === AutofillOverlayElement.Button) { if (message?.overlayElement === AutofillOverlayElement.Button) {
this.removeInlineMenuButton(); this.removeInlineMenuButton();
return; return;
} }
if (message.overlayElement === AutofillOverlayElement.List) { if (message?.overlayElement === AutofillOverlayElement.List) {
this.removeInlineMenuList(); this.removeInlineMenuList();
return; return;
} }
@ -408,7 +408,7 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
clearTimeout(this.mutationObserverIterationsResetTimeout); clearTimeout(this.mutationObserverIterationsResetTimeout);
this.mutationObserverIterations = 0; this.mutationObserverIterations = 0;
void this.sendExtensionMessage("blurMostRecentOverlayField"); void this.sendExtensionMessage("blurMostRecentOverlayField");
this.removeInlineMenu({ forceClose: true }); this.removeInlineMenu();
return true; return true;
} }
@ -417,6 +417,6 @@ export class InlineMenuElements implements InlineMenuElementsInterface {
} }
destroy() { destroy() {
this.documentElementMutationObserver?.disconnect(); this.documentElementMutationObserver?.disconnect();
this.removeInlineMenu({ forceClose: true }); this.removeInlineMenu();
} }
} }

View File

@ -1,17 +1,15 @@
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { setElementStyles } from "../../utils"; import { sendExtensionMessage, setElementStyles } from "../../utils";
import { import {
BackgroundPortMessageHandlers, BackgroundPortMessageHandlers,
AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface, AutofillOverlayIframeService as AutofillOverlayIframeServiceInterface,
AutofillOverlayIframeExtensionMessage, AutofillOverlayIframeExtensionMessage,
AutofillOverlayIframeWindowMessageHandlers,
} from "../abstractions/autofill-overlay-iframe.service"; } from "../abstractions/autofill-overlay-iframe.service";
class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterface { class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterface {
private port: chrome.runtime.Port | null = null; private port: chrome.runtime.Port | null = null;
private extensionOriginsSet: Set<string>;
private iframeMutationObserver: MutationObserver; private iframeMutationObserver: MutationObserver;
private iframe: HTMLIFrameElement; private iframe: HTMLIFrameElement;
private ariaAlertElement: HTMLDivElement; private ariaAlertElement: HTMLDivElement;
@ -42,15 +40,11 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
private foreignMutationsCount = 0; private foreignMutationsCount = 0;
private mutationObserverIterations = 0; private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private readonly windowMessageHandlers: AutofillOverlayIframeWindowMessageHandlers = {
updateAutofillOverlayListHeight: (message) =>
this.updateElementStyles(this.iframe, message.styles),
getPageColorScheme: () => this.updateOverlayPageColorScheme(),
};
private readonly backgroundPortMessageHandlers: BackgroundPortMessageHandlers = { private readonly backgroundPortMessageHandlers: BackgroundPortMessageHandlers = {
initAutofillOverlayList: ({ message }) => this.initAutofillOverlayList(message), initAutofillOverlayList: ({ message }) => this.initAutofillOverlayList(message),
updateIframePosition: ({ message }) => this.updateIframePosition(message.styles), updateIframePosition: ({ message }) => this.updateIframePosition(message.styles),
updateOverlayHidden: ({ message }) => this.updateElementStyles(this.iframe, message.styles), updateOverlayHidden: ({ message }) => this.updateElementStyles(this.iframe, message.styles),
getPageColorScheme: () => this.updateOverlayPageColorScheme(),
}; };
constructor( constructor(
@ -58,11 +52,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
private portName: string, private portName: string,
private shadow: ShadowRoot, private shadow: ShadowRoot,
) { ) {
this.extensionOriginsSet = new Set([
chrome.runtime.getURL("").slice(0, -1).toLowerCase(), // Remove the trailing slash and normalize the extension url to lowercase
"null",
]);
this.iframeMutationObserver = new MutationObserver(this.handleMutations); this.iframeMutationObserver = new MutationObserver(this.handleMutations);
} }
@ -134,7 +123,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
this.port = chrome.runtime.connect({ name: this.portName }); this.port = chrome.runtime.connect({ name: this.portName });
this.port.onDisconnect.addListener(this.handlePortDisconnect); this.port.onDisconnect.addListener(this.handlePortDisconnect);
this.port.onMessage.addListener(this.handlePortMessage); this.port.onMessage.addListener(this.handlePortMessage);
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessage);
this.announceAriaAlert(); this.announceAriaAlert();
}; };
@ -168,7 +156,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
} }
this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" }); this.updateElementStyles(this.iframe, { opacity: "0", height: "0px", display: "block" });
globalThis.removeEventListener("message", this.handleWindowMessage);
this.unobserveIframe(); this.unobserveIframe();
this.port?.onMessage.removeListener(this.handlePortMessage); this.port?.onMessage.removeListener(this.handlePortMessage);
this.port?.onDisconnect.removeListener(this.handlePortDisconnect); this.port?.onDisconnect.removeListener(this.handlePortDisconnect);
@ -265,30 +252,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
); );
} }
/**
* Handles messages sent from the iframe. If the message does not have a
* specified handler set, it passes the message to the background script.
*
* @param event - The message event
*/
private handleWindowMessage = (event: MessageEvent) => {
if (
!this.port ||
event.source !== this.iframe.contentWindow ||
!this.isFromExtensionOrigin(event.origin.toLowerCase())
) {
return;
}
const message = event.data;
if (this.windowMessageHandlers[message.command]) {
this.windowMessageHandlers[message.command](message);
return;
}
this.port.postMessage(event.data);
};
/** /**
* Accepts an element and updates the styles for that element. This method * Accepts an element and updates the styles for that element. This method
* will also unobserve the element if it is the iframe element. This is * will also unobserve the element if it is the iframe element. This is
@ -311,17 +274,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
this.observeIframe(); this.observeIframe();
} }
/**
* Chrome returns null for any sandboxed iframe sources.
* Firefox references the extension URI as its origin.
* Any other origin value is a security risk.
*
* @param messageOrigin - The origin of the window message
*/
private isFromExtensionOrigin(messageOrigin: string): boolean {
return this.extensionOriginsSet.has(messageOrigin);
}
/** /**
* Handles mutations to the iframe element. The ensures that the iframe * Handles mutations to the iframe element. The ensures that the iframe
* element's styles are not modified by a third party source. * element's styles are not modified by a third party source.
@ -351,6 +303,10 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
} }
}; };
private forceCloseAutofillOverlay() {
void sendExtensionMessage("closeAutofillOverlay", { forceClose: true });
}
/** /**
* Handles mutations to the iframe element's attributes. This ensures that * Handles mutations to the iframe element's attributes. This ensures that
* the iframe element's attributes are not modified by a third party source. * the iframe element's attributes are not modified by a third party source.
@ -366,7 +322,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
} }
if (this.foreignMutationsCount >= 10) { if (this.foreignMutationsCount >= 10) {
this.port?.postMessage({ command: "forceCloseAutofillOverlay" }); this.forceCloseAutofillOverlay();
break; break;
} }
@ -421,7 +377,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
if (this.mutationObserverIterations > 20) { if (this.mutationObserverIterations > 20) {
clearTimeout(this.mutationObserverIterationsResetTimeout); clearTimeout(this.mutationObserverIterationsResetTimeout);
resetCounters(); resetCounters();
this.port?.postMessage({ command: "forceCloseAutofillOverlay" }); this.forceCloseAutofillOverlay();
return true; return true;
} }

View File

@ -46,15 +46,20 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
* @param authStatus - The authentication status of the user * @param authStatus - The authentication status of the user
* @param styleSheetUrl - The URL of the stylesheet to apply to the page * @param styleSheetUrl - The URL of the stylesheet to apply to the page
* @param translations - The translations to apply to the page * @param translations - The translations to apply to the page
* @private * @param messageConnectorUrl - The URL of the message connector to use
*/ */
private async initAutofillOverlayButton({ private async initAutofillOverlayButton({
authStatus, authStatus,
styleSheetUrl, styleSheetUrl,
translations, translations,
messageConnectorUrl,
}: InitAutofillOverlayButtonMessage) { }: InitAutofillOverlayButtonMessage) {
const linkElement = this.initOverlayPage("button", styleSheetUrl, translations); const linkElement = await this.initOverlayPage(
"button",
styleSheetUrl,
translations,
messageConnectorUrl,
);
this.buttonElement.tabIndex = -1; this.buttonElement.tabIndex = -1;
this.buttonElement.type = "button"; this.buttonElement.type = "button";
this.buttonElement.classList.add("overlay-button"); this.buttonElement.classList.add("overlay-button");
@ -63,7 +68,7 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
this.getTranslation("toggleBitwardenVaultOverlay"), this.getTranslation("toggleBitwardenVaultOverlay"),
); );
this.buttonElement.addEventListener(EVENTS.CLICK, this.handleButtonElementClick); this.buttonElement.addEventListener(EVENTS.CLICK, this.handleButtonElementClick);
this.postMessageToParent({ command: "getPageColorScheme" }); this.postMessageToConnector({ command: "getPageColorScheme" });
this.updateAuthStatus(authStatus); this.updateAuthStatus(authStatus);
@ -103,7 +108,7 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
* parent window indicating that the button was clicked. * parent window indicating that the button was clicked.
*/ */
private handleButtonElementClick = () => { private handleButtonElementClick = () => {
this.postMessageToParent({ command: "overlayButtonClicked" }); this.postMessageToConnector({ command: "overlayButtonClicked" });
}; };
/** /**
@ -115,7 +120,7 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
return; return;
} }
this.postMessageToParent({ command: "closeAutofillOverlay" }); this.postMessageToConnector({ command: "closeAutofillOverlay" });
} }
} }

View File

@ -44,6 +44,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
* @param theme - The theme to use for the overlay list. * @param theme - The theme to use for the overlay list.
* @param authStatus - The current authentication status. * @param authStatus - The current authentication status.
* @param ciphers - The ciphers to display in the overlay list. * @param ciphers - The ciphers to display in the overlay list.
* @param messageConnectorUrl - The URL of the message connector to use.
*/ */
private async initAutofillOverlayList({ private async initAutofillOverlayList({
translations, translations,
@ -51,8 +52,14 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
theme, theme,
authStatus, authStatus,
ciphers, ciphers,
messageConnectorUrl,
}: InitAutofillOverlayListMessage) { }: InitAutofillOverlayListMessage) {
const linkElement = this.initOverlayPage("button", styleSheetUrl, translations); const linkElement = await this.initOverlayPage(
"list",
styleSheetUrl,
translations,
messageConnectorUrl,
);
const themeClass = `theme_${theme}`; const themeClass = `theme_${theme}`;
globalThis.document.documentElement.classList.add(themeClass); globalThis.document.documentElement.classList.add(themeClass);
@ -105,7 +112,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
* Sends a message to the parent window to unlock the vault. * Sends a message to the parent window to unlock the vault.
*/ */
private handleUnlockButtonClick = () => { private handleUnlockButtonClick = () => {
this.postMessageToParent({ command: "unlockVault" }); this.postMessageToConnector({ command: "unlockVault" });
}; };
/** /**
@ -169,7 +176,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
* Sends a message to the parent window to add a new vault item. * Sends a message to the parent window to add a new vault item.
*/ */
private handeNewItemButtonClick = () => { private handeNewItemButtonClick = () => {
this.postMessageToParent({ command: "addNewVaultItem" }); this.postMessageToConnector({ command: "addNewVaultItem" });
}; };
/** /**
@ -276,7 +283,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
private handleFillCipherClickEvent = (cipher: OverlayCipherData) => { private handleFillCipherClickEvent = (cipher: OverlayCipherData) => {
return this.useEventHandlersMemo( return this.useEventHandlersMemo(
() => () =>
this.postMessageToParent({ this.postMessageToConnector({
command: "fillSelectedListItem", command: "fillSelectedListItem",
overlayCipherId: cipher.id, overlayCipherId: cipher.id,
}), }),
@ -341,7 +348,8 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
*/ */
private handleViewCipherClickEvent = (cipher: OverlayCipherData) => { private handleViewCipherClickEvent = (cipher: OverlayCipherData) => {
return this.useEventHandlersMemo( return this.useEventHandlersMemo(
() => this.postMessageToParent({ command: "viewSelectedCipher", overlayCipherId: cipher.id }), () =>
this.postMessageToConnector({ command: "viewSelectedCipher", overlayCipherId: cipher.id }),
`${cipher.id}-view-cipher-button-click-handler`, `${cipher.id}-view-cipher-button-click-handler`,
); );
}; };
@ -476,7 +484,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
return; return;
} }
this.postMessageToParent({ command: "checkAutofillOverlayButtonFocused" }); this.postMessageToConnector({ command: "checkAutofillOverlayButtonFocused" });
} }
/** /**
@ -533,7 +541,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
} }
const { height } = entry.contentRect; const { height } = entry.contentRect;
this.postMessageToParent({ this.postMessageToConnector({
command: "updateAutofillOverlayListHeight", command: "updateAutofillOverlayListHeight",
styles: { height: `${height}px` }, styles: { height: `${height}px` },
}); });

View File

@ -0,0 +1,3 @@
import { AutofillOverlayMessageConnector } from "./message-connector";
(() => new AutofillOverlayMessageConnector())();

View File

@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<title>Autofill overlay message connector</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="color-scheme" content="normal" />
</head>
<body></body>
</html>

View File

@ -0,0 +1,46 @@
export class AutofillOverlayMessageConnector {
private extensionOriginsSet: Set<string>;
private port: chrome.runtime.Port | null = null;
constructor() {
globalThis.addEventListener("message", this.handleWindowMessage);
this.extensionOriginsSet = new Set([
chrome.runtime.getURL("").slice(0, -1).toLowerCase(), // Remove the trailing slash and normalize the extension url to lowercase
"null",
]);
}
private handleWindowMessage = (event: MessageEvent) => {
if (
event.source !== globalThis.parent ||
!this.isFromExtensionOrigin(event.origin.toLowerCase())
) {
return;
}
const message = event.data;
if (this.port) {
this.port.postMessage(message);
return;
}
if (message.command !== "initAutofillOverlayPort") {
return;
}
this.port = chrome.runtime.connect({ name: message.portName });
};
/**
* Chrome returns null for any sandboxed iframe sources.
* Firefox references the extension URI as its origin.
* Any other origin value is a security risk.
*
* @param messageOrigin - The origin of the window message
*/
private isFromExtensionOrigin(messageOrigin: string): boolean {
return this.extensionOriginsSet.has(messageOrigin);
}
}

View File

@ -31,11 +31,12 @@ describe("AutofillOverlayPageElement", () => {
jest.spyOn(globalThis.document, "createElement"); jest.spyOn(globalThis.document, "createElement");
}); });
it("initializes the button overlay page", () => { it("initializes the button overlay page", async () => {
const linkElement = autofillOverlayPageElement["initOverlayPage"]( const linkElement = await autofillOverlayPageElement["initOverlayPage"](
"button", "button",
"https://jest-testing-website.com", "https://jest-testing-website.com",
translations, translations,
"https://jest-testing-website.com/message-connector",
); );
expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith( expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith(
@ -49,16 +50,16 @@ describe("AutofillOverlayPageElement", () => {
}); });
}); });
describe("postMessageToParent", () => { describe("postMessageToConnector", () => {
it("skips posting a message to the parent if the message origin in not set", () => { it("skips posting a message to the parent if the message origin in not set", () => {
autofillOverlayPageElement["postMessageToParent"]({ command: "test" }); autofillOverlayPageElement["postMessageToConnector"]({ command: "test" });
expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
}); });
it("posts a message to the parent", () => { it("posts a message to the parent", () => {
autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com"; autofillOverlayPageElement["messageOrigin"] = "https://jest-testing-website.com";
autofillOverlayPageElement["postMessageToParent"]({ command: "test" }); autofillOverlayPageElement["postMessageToConnector"]({ command: "test" });
expect(globalThis.parent.postMessage).toHaveBeenCalledWith( expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
{ command: "test" }, { command: "test" },

View File

@ -1,6 +1,6 @@
import { EVENTS } from "@bitwarden/common/autofill/constants"; import { EVENTS } from "@bitwarden/common/autofill/constants";
import { RedirectFocusDirection } from "../../../utils/autofill-overlay.enum"; import { AutofillOverlayPort, RedirectFocusDirection } from "../../../utils/autofill-overlay.enum";
import { import {
AutofillOverlayPageElementWindowMessage, AutofillOverlayPageElementWindowMessage,
WindowMessageHandlers, WindowMessageHandlers,
@ -10,6 +10,7 @@ class AutofillOverlayPageElement extends HTMLElement {
protected shadowDom: ShadowRoot; protected shadowDom: ShadowRoot;
protected messageOrigin: string; protected messageOrigin: string;
protected translations: Record<string, string>; protected translations: Record<string, string>;
protected messageConnectorIframe: HTMLIFrameElement;
protected windowMessageHandlers: WindowMessageHandlers; protected windowMessageHandlers: WindowMessageHandlers;
constructor() { constructor() {
@ -25,16 +26,41 @@ class AutofillOverlayPageElement extends HTMLElement {
* @param elementName - The name of the element, e.g. "button" or "list" * @param elementName - The name of the element, e.g. "button" or "list"
* @param styleSheetUrl - The URL of the stylesheet to apply to the page * @param styleSheetUrl - The URL of the stylesheet to apply to the page
* @param translations - The translations to apply to the page * @param translations - The translations to apply to the page
* @param messageConnectorUrl - The URL of the message connector to use
*/ */
protected initOverlayPage( protected async initOverlayPage(
elementName: "button" | "list", elementName: "button" | "list",
styleSheetUrl: string, styleSheetUrl: string,
translations: Record<string, string>, translations: Record<string, string>,
): HTMLLinkElement { messageConnectorUrl: string,
): Promise<HTMLLinkElement> {
this.translations = translations; this.translations = translations;
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale")); globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
globalThis.document.head.title = this.getTranslation(`${elementName}PageTitle`); globalThis.document.head.title = this.getTranslation(`${elementName}PageTitle`);
this.messageConnectorIframe = globalThis.document.createElement("iframe");
this.messageConnectorIframe.src = messageConnectorUrl;
this.messageConnectorIframe.style.opacity = "0";
this.messageConnectorIframe.style.position = "absolute";
this.messageConnectorIframe.style.width = "0";
this.messageConnectorIframe.style.height = "0";
this.messageConnectorIframe.style.border = "none";
this.messageConnectorIframe.style.pointerEvents = "none";
globalThis.document.body.appendChild(this.messageConnectorIframe);
await new Promise<void>((resolve) => {
this.messageConnectorIframe.addEventListener(EVENTS.LOAD, () => {
this.postMessageToConnector({
command: `initAutofillOverlayPort`,
portName:
elementName === "list"
? AutofillOverlayPort.ListMessageConnector
: AutofillOverlayPort.ButtonMessageConnector,
});
resolve();
});
});
this.shadowDom.innerHTML = ""; this.shadowDom.innerHTML = "";
const linkElement = globalThis.document.createElement("link"); const linkElement = globalThis.document.createElement("link");
linkElement.setAttribute("rel", "stylesheet"); linkElement.setAttribute("rel", "stylesheet");
@ -48,12 +74,12 @@ class AutofillOverlayPageElement extends HTMLElement {
* *
* @param message - The message to post * @param message - The message to post
*/ */
protected postMessageToParent(message: AutofillOverlayPageElementWindowMessage) { protected postMessageToConnector(message: AutofillOverlayPageElementWindowMessage) {
if (!this.messageOrigin) { if (!this.messageOrigin) {
return; return;
} }
globalThis.parent.postMessage(message, this.messageOrigin); this.messageConnectorIframe.contentWindow.postMessage(message, "*");
} }
/** /**
@ -111,7 +137,7 @@ class AutofillOverlayPageElement extends HTMLElement {
* Handles the window blur event. * Handles the window blur event.
*/ */
private handleWindowBlurEvent = () => { private handleWindowBlurEvent = () => {
this.postMessageToParent({ command: "overlayPageBlurred" }); this.postMessageToConnector({ command: "overlayPageBlurred" });
}; };
/** /**
@ -148,7 +174,7 @@ class AutofillOverlayPageElement extends HTMLElement {
* @param direction - The direction to redirect the focus out * @param direction - The direction to redirect the focus out
*/ */
private redirectOverlayFocusOutMessage(direction: string) { private redirectOverlayFocusOutMessage(direction: string) {
this.postMessageToParent({ command: "redirectOverlayFocusOut", direction }); this.postMessageToConnector({ command: "redirectOverlayFocusOut", direction });
} }
} }

View File

@ -173,6 +173,7 @@ function createInitAutofillOverlayButtonMessageMock(
translations: overlayPagesTranslations, translations: overlayPagesTranslations,
styleSheetUrl: "https://jest-testing-website.com", styleSheetUrl: "https://jest-testing-website.com",
authStatus: AuthenticationStatus.Unlocked, authStatus: AuthenticationStatus.Unlocked,
messageConnectorUrl: "https://jest-testing-website.com/message-connector",
...customFields, ...customFields,
}; };
} }
@ -203,6 +204,7 @@ function createInitAutofillOverlayListMessageMock(
styleSheetUrl: "https://jest-testing-website.com", styleSheetUrl: "https://jest-testing-website.com",
theme: ThemeType.Light, theme: ThemeType.Light,
authStatus: AuthenticationStatus.Unlocked, authStatus: AuthenticationStatus.Unlocked,
messageConnectorUrl: "https://jest-testing-website.com/message-connector",
ciphers: [ ciphers: [
createAutofillOverlayCipherDataMock(1, { createAutofillOverlayCipherDataMock(1, {
icon: { icon: {

View File

@ -5,7 +5,9 @@ const AutofillOverlayElement = {
const AutofillOverlayPort = { const AutofillOverlayPort = {
Button: "autofill-overlay-button-port", Button: "autofill-overlay-button-port",
ButtonMessageConnector: "autofill-overlay-button-message-connector",
List: "autofill-overlay-list-port", List: "autofill-overlay-list-port",
ListMessageConnector: "autofill-overlay-list-message-connector",
} as const; } as const;
const RedirectFocusDirection = { const RedirectFocusDirection = {

View File

@ -115,6 +115,7 @@
"images/icon38_locked.png", "images/icon38_locked.png",
"overlay/button.html", "overlay/button.html",
"overlay/list.html", "overlay/list.html",
"overlay/message-connector.html",
"popup/fonts/*" "popup/fonts/*"
], ],
"applications": { "applications": {

View File

@ -119,6 +119,7 @@
"images/icon38_locked.png", "images/icon38_locked.png",
"overlay/button.html", "overlay/button.html",
"overlay/list.html", "overlay/list.html",
"overlay/message-connector.html",
"popup/fonts/*" "popup/fonts/*"
], ],
"matches": ["<all_urls>"] "matches": ["<all_urls>"]

View File

@ -116,6 +116,11 @@ const plugins = [
filename: "overlay/list.html", filename: "overlay/list.html",
chunks: ["overlay/list"], chunks: ["overlay/list"],
}), }),
new HtmlWebpackPlugin({
template: "./src/autofill/overlay/pages/message-connector/message-connector.html",
filename: "overlay/message-connector.html",
chunks: ["overlay/message-connector"],
}),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: [ patterns: [
manifestVersion == 3 manifestVersion == 3
@ -173,6 +178,8 @@ const mainConfig = {
"notification/bar": "./src/autofill/notification/bar.ts", "notification/bar": "./src/autofill/notification/bar.ts",
"overlay/button": "./src/autofill/overlay/pages/button/bootstrap-autofill-overlay-button.ts", "overlay/button": "./src/autofill/overlay/pages/button/bootstrap-autofill-overlay-button.ts",
"overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts", "overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts",
"overlay/message-connector":
"./src/autofill/overlay/pages/message-connector/bootstrap-autofill-overlay-message-connector.ts",
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
"content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts", "content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts",
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",