1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

[PM-5189] Implementing a verification process to ensure we are receiving valid inline menu messages within the background script

This commit is contained in:
Cesar Gonzalez
2024-04-08 07:02:33 -05:00
parent a69d9d8f4a
commit dd6f3d46cb
11 changed files with 60 additions and 25 deletions

View File

@@ -48,6 +48,7 @@ type FocusedFieldData = {
type OverlayBackgroundExtensionMessage = { type OverlayBackgroundExtensionMessage = {
command: string; command: string;
portKey?: string;
tab?: chrome.tabs.Tab; tab?: chrome.tabs.Tab;
sender?: string; sender?: string;
details?: AutofillPageDetails; details?: AutofillPageDetails;

View File

@@ -27,6 +27,7 @@ import {
openViewVaultItemPopout, openViewVaultItemPopout,
} from "../../vault/popup/utils/vault-popout-window"; } from "../../vault/popup/utils/vault-popout-window";
import { AutofillService } from "../services/abstractions/autofill.service"; import { AutofillService } from "../services/abstractions/autofill.service";
import { generateRandomChars } from "../utils";
import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum"; import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
@@ -56,6 +57,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
private overlayButtonPort: chrome.runtime.Port; private overlayButtonPort: chrome.runtime.Port;
private overlayListPort: chrome.runtime.Port; private overlayListPort: chrome.runtime.Port;
private portKeyForTab: Record<number, string> = {};
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFocused: boolean = false;
private isFieldCurrentlyFilling: boolean = false; private isFieldCurrentlyFilling: boolean = false;
@@ -140,6 +142,10 @@ class OverlayBackground implements OverlayBackgroundInterface {
this.subFrameOffsetsForTab[tabId].clear(); this.subFrameOffsetsForTab[tabId].clear();
delete this.subFrameOffsetsForTab[tabId]; delete this.subFrameOffsetsForTab[tabId];
} }
if (this.portKeyForTab[tabId]) {
delete this.portKeyForTab[tabId];
}
} }
/** /**
@@ -635,7 +641,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
*/ */
private async getAuthStatus() { private async getAuthStatus() {
const formerAuthStatus = this.userAuthStatus; const formerAuthStatus = this.userAuthStatus;
this.userAuthStatus = await this.authService.getAuthStatus(); this.userAuthStatus = await firstValueFrom(this.authService.activeAccountStatus$);
if ( if (
this.userAuthStatus !== formerAuthStatus && this.userAuthStatus !== formerAuthStatus &&
@@ -939,11 +945,15 @@ class OverlayBackground implements OverlayBackgroundInterface {
const isOverlayListPort = port.name === AutofillOverlayPort.List; const isOverlayListPort = port.name === AutofillOverlayPort.List;
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
if (!isOverlayListPort && !isOverlayButtonPort) { if (!isOverlayListPort && !isOverlayButtonPort) {
return; return;
} }
const tabId = port.sender.tab.id;
if (!this.portKeyForTab[tabId]) {
this.portKeyForTab[tabId] = generateRandomChars(12);
}
if (isOverlayListPort) { if (isOverlayListPort) {
this.overlayListPort = port; this.overlayListPort = port;
} else { } else {
@@ -959,6 +969,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
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"), messageConnectorUrl: chrome.runtime.getURL("overlay/message-connector.html"),
portKey: this.portKeyForTab[tabId],
}); });
void this.updateOverlayPosition( void this.updateOverlayPosition(
{ {
@@ -980,7 +991,12 @@ class OverlayBackground implements OverlayBackgroundInterface {
message: OverlayBackgroundExtensionMessage, message: OverlayBackgroundExtensionMessage,
port: chrome.runtime.Port, port: chrome.runtime.Port,
) => { ) => {
const command = message?.command; const tabId = port.sender.tab.id;
if (this.portKeyForTab[tabId] !== message?.portKey) {
return;
}
const command = message.command;
let handler: CallableFunction | undefined; let handler: CallableFunction | undefined;
if (port.name === AutofillOverlayPort.ButtonMessageConnector) { if (port.name === AutofillOverlayPort.ButtonMessageConnector) {

View File

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

View File

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

View File

@@ -47,18 +47,21 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
* @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 * @param messageConnectorUrl - The URL of the message connector to use
* @param portKey - Background generated key that allows the port to communicate with the background
*/ */
private async initAutofillOverlayButton({ private async initAutofillOverlayButton({
authStatus, authStatus,
styleSheetUrl, styleSheetUrl,
translations, translations,
messageConnectorUrl, messageConnectorUrl,
portKey,
}: InitAutofillOverlayButtonMessage) { }: InitAutofillOverlayButtonMessage) {
const linkElement = await this.initOverlayPage( const linkElement = await this.initOverlayPage(
"button", "button",
styleSheetUrl, styleSheetUrl,
translations, translations,
messageConnectorUrl, messageConnectorUrl,
portKey,
); );
this.buttonElement.tabIndex = -1; this.buttonElement.tabIndex = -1;
this.buttonElement.type = "button"; this.buttonElement.type = "button";

View File

@@ -45,6 +45,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
* @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. * @param messageConnectorUrl - The URL of the message connector to use.
* @param portKey - Background generated key that allows the port to communicate with the background.
*/ */
private async initAutofillOverlayList({ private async initAutofillOverlayList({
translations, translations,
@@ -53,12 +54,14 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
authStatus, authStatus,
ciphers, ciphers,
messageConnectorUrl, messageConnectorUrl,
portKey,
}: InitAutofillOverlayListMessage) { }: InitAutofillOverlayListMessage) {
const linkElement = await this.initOverlayPage( const linkElement = await this.initOverlayPage(
"list", "list",
styleSheetUrl, styleSheetUrl,
translations, translations,
messageConnectorUrl, messageConnectorUrl,
portKey,
); );
const themeClass = `theme_${theme}`; const themeClass = `theme_${theme}`;

View File

@@ -12,25 +12,23 @@ export class AutofillOverlayMessageConnector {
} }
private handleWindowMessage = (event: MessageEvent) => { private handleWindowMessage = (event: MessageEvent) => {
const message = event.data;
if ( if (
event.source !== globalThis.parent || event.source !== globalThis.parent ||
!this.isFromExtensionOrigin(event.origin.toLowerCase()) !this.isFromExtensionOrigin(event.origin.toLowerCase()) ||
!message.portKey
) { ) {
return; return;
} }
const message = event.data;
if (this.port) { if (this.port) {
this.port.postMessage(message); this.port.postMessage(message);
return; return;
} }
if (message.command !== "initAutofillOverlayPort") { if (message.command === "initAutofillOverlayPort") {
return; this.port = chrome.runtime.connect({ name: message.portName });
} }
this.port = chrome.runtime.connect({ name: message.portName });
}; };
/** /**

View File

@@ -37,6 +37,7 @@ describe("AutofillOverlayPageElement", () => {
"https://jest-testing-website.com", "https://jest-testing-website.com",
translations, translations,
"https://jest-testing-website.com/message-connector", "https://jest-testing-website.com/message-connector",
"portKey",
); );
expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith( expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith(

View File

@@ -11,6 +11,7 @@ class AutofillOverlayPageElement extends HTMLElement {
protected messageOrigin: string; protected messageOrigin: string;
protected translations: Record<string, string>; protected translations: Record<string, string>;
protected messageConnectorIframe: HTMLIFrameElement; protected messageConnectorIframe: HTMLIFrameElement;
private portKey: string;
protected windowMessageHandlers: WindowMessageHandlers; protected windowMessageHandlers: WindowMessageHandlers;
constructor() { constructor() {
@@ -27,13 +28,17 @@ class AutofillOverlayPageElement extends HTMLElement {
* @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 * @param messageConnectorUrl - The URL of the message connector to use
* @param portKey - Background generated key that allows the port to communicate with the background
*/ */
protected async initOverlayPage( protected async initOverlayPage(
elementName: "button" | "list", elementName: "button" | "list",
styleSheetUrl: string, styleSheetUrl: string,
translations: Record<string, string>, translations: Record<string, string>,
messageConnectorUrl: string, messageConnectorUrl: string,
portKey: string,
): Promise<HTMLLinkElement> { ): Promise<HTMLLinkElement> {
this.portKey = portKey;
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`);
@@ -79,7 +84,10 @@ class AutofillOverlayPageElement extends HTMLElement {
return; return;
} }
this.messageConnectorIframe.contentWindow.postMessage(message, "*"); this.messageConnectorIframe.contentWindow.postMessage(
{ portKey: this.portKey, ...message },
"*",
);
} }
/** /**

View File

@@ -174,6 +174,7 @@ function createInitAutofillOverlayButtonMessageMock(
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", messageConnectorUrl: "https://jest-testing-website.com/message-connector",
portKey: "portKey",
...customFields, ...customFields,
}; };
} }
@@ -205,6 +206,7 @@ function createInitAutofillOverlayListMessageMock(
theme: ThemeType.Light, theme: ThemeType.Light,
authStatus: AuthenticationStatus.Unlocked, authStatus: AuthenticationStatus.Unlocked,
messageConnectorUrl: "https://jest-testing-website.com/message-connector", messageConnectorUrl: "https://jest-testing-website.com/message-connector",
portKey: "portKey",
ciphers: [ ciphers: [
createAutofillOverlayCipherDataMock(1, { createAutofillOverlayCipherDataMock(1, {
icon: { icon: {

View File

@@ -1,24 +1,24 @@
import { AutofillPort } from "../enums/autofill-port.enums"; import { AutofillPort } from "../enums/autofill-port.enums";
import { FillableFormFieldElement, FormFieldElement } from "../types"; import { FillableFormFieldElement, FormFieldElement } from "../types";
function generateRandomChars(length: number): string {
const chars = "abcdefghijklmnopqrstuvwxyz";
const randomChars = [];
const randomBytes = new Uint8Array(length);
globalThis.crypto.getRandomValues(randomBytes);
for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) {
const byte = randomBytes[byteIndex];
randomChars.push(chars[byte % chars.length]);
}
return randomChars.join("");
}
/** /**
* Generates a random string of characters that formatted as a custom element name. * Generates a random string of characters that formatted as a custom element name.
*/ */
function generateRandomCustomElementName(): string { function generateRandomCustomElementName(): string {
const generateRandomChars = (length: number): string => {
const chars = "abcdefghijklmnopqrstuvwxyz";
const randomChars = [];
const randomBytes = new Uint8Array(length);
globalThis.crypto.getRandomValues(randomBytes);
for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) {
const byte = randomBytes[byteIndex];
randomChars.push(chars[byte % chars.length]);
}
return randomChars.join("");
};
const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters
const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens
@@ -274,6 +274,7 @@ function nodeIsFormElement(node: Node): node is HTMLFormElement {
} }
export { export {
generateRandomChars,
generateRandomCustomElementName, generateRandomCustomElementName,
buildSvgDomElement, buildSvgDomElement,
sendExtensionMessage, sendExtensionMessage,