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:
@@ -48,6 +48,7 @@ type FocusedFieldData = {
|
||||
|
||||
type OverlayBackgroundExtensionMessage = {
|
||||
command: string;
|
||||
portKey?: string;
|
||||
tab?: chrome.tabs.Tab;
|
||||
sender?: string;
|
||||
details?: AutofillPageDetails;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
openViewVaultItemPopout,
|
||||
} from "../../vault/popup/utils/vault-popout-window";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { generateRandomChars } from "../utils";
|
||||
import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum";
|
||||
|
||||
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
|
||||
@@ -56,6 +57,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
|
||||
private overlayButtonPort: chrome.runtime.Port;
|
||||
private overlayListPort: chrome.runtime.Port;
|
||||
private portKeyForTab: Record<number, string> = {};
|
||||
private focusedFieldData: FocusedFieldData;
|
||||
private isFieldCurrentlyFocused: boolean = false;
|
||||
private isFieldCurrentlyFilling: boolean = false;
|
||||
@@ -140,6 +142,10 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
this.subFrameOffsetsForTab[tabId].clear();
|
||||
delete this.subFrameOffsetsForTab[tabId];
|
||||
}
|
||||
|
||||
if (this.portKeyForTab[tabId]) {
|
||||
delete this.portKeyForTab[tabId];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -635,7 +641,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
*/
|
||||
private async getAuthStatus() {
|
||||
const formerAuthStatus = this.userAuthStatus;
|
||||
this.userAuthStatus = await this.authService.getAuthStatus();
|
||||
this.userAuthStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
|
||||
if (
|
||||
this.userAuthStatus !== formerAuthStatus &&
|
||||
@@ -939,11 +945,15 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
const isOverlayListPort = port.name === AutofillOverlayPort.List;
|
||||
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
|
||||
|
||||
if (!isOverlayListPort && !isOverlayButtonPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = port.sender.tab.id;
|
||||
if (!this.portKeyForTab[tabId]) {
|
||||
this.portKeyForTab[tabId] = generateRandomChars(12);
|
||||
}
|
||||
|
||||
if (isOverlayListPort) {
|
||||
this.overlayListPort = port;
|
||||
} else {
|
||||
@@ -959,6 +969,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
translations: this.getTranslations(),
|
||||
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
|
||||
messageConnectorUrl: chrome.runtime.getURL("overlay/message-connector.html"),
|
||||
portKey: this.portKeyForTab[tabId],
|
||||
});
|
||||
void this.updateOverlayPosition(
|
||||
{
|
||||
@@ -980,7 +991,12 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
message: OverlayBackgroundExtensionMessage,
|
||||
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;
|
||||
|
||||
if (port.name === AutofillOverlayPort.ButtonMessageConnector) {
|
||||
|
||||
@@ -8,6 +8,7 @@ type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & {
|
||||
styleSheetUrl: string;
|
||||
translations: Record<string, string>;
|
||||
messageConnectorUrl: string;
|
||||
portKey: string;
|
||||
};
|
||||
|
||||
type OverlayButtonWindowMessageHandlers = {
|
||||
|
||||
@@ -15,6 +15,7 @@ type InitAutofillOverlayListMessage = OverlayListMessage & {
|
||||
translations: Record<string, string>;
|
||||
ciphers?: OverlayCipherData[];
|
||||
messageConnectorUrl: string;
|
||||
portKey: string;
|
||||
};
|
||||
|
||||
type OverlayListWindowMessageHandlers = {
|
||||
|
||||
@@ -47,18 +47,21 @@ class AutofillOverlayButton extends AutofillOverlayPageElement {
|
||||
* @param styleSheetUrl - The URL of the stylesheet 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 portKey - Background generated key that allows the port to communicate with the background
|
||||
*/
|
||||
private async initAutofillOverlayButton({
|
||||
authStatus,
|
||||
styleSheetUrl,
|
||||
translations,
|
||||
messageConnectorUrl,
|
||||
portKey,
|
||||
}: InitAutofillOverlayButtonMessage) {
|
||||
const linkElement = await this.initOverlayPage(
|
||||
"button",
|
||||
styleSheetUrl,
|
||||
translations,
|
||||
messageConnectorUrl,
|
||||
portKey,
|
||||
);
|
||||
this.buttonElement.tabIndex = -1;
|
||||
this.buttonElement.type = "button";
|
||||
|
||||
@@ -45,6 +45,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
* @param authStatus - The current authentication status.
|
||||
* @param ciphers - The ciphers to display in the overlay list.
|
||||
* @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({
|
||||
translations,
|
||||
@@ -53,12 +54,14 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
||||
authStatus,
|
||||
ciphers,
|
||||
messageConnectorUrl,
|
||||
portKey,
|
||||
}: InitAutofillOverlayListMessage) {
|
||||
const linkElement = await this.initOverlayPage(
|
||||
"list",
|
||||
styleSheetUrl,
|
||||
translations,
|
||||
messageConnectorUrl,
|
||||
portKey,
|
||||
);
|
||||
|
||||
const themeClass = `theme_${theme}`;
|
||||
|
||||
@@ -12,25 +12,23 @@ export class AutofillOverlayMessageConnector {
|
||||
}
|
||||
|
||||
private handleWindowMessage = (event: MessageEvent) => {
|
||||
const message = event.data;
|
||||
if (
|
||||
event.source !== globalThis.parent ||
|
||||
!this.isFromExtensionOrigin(event.origin.toLowerCase())
|
||||
!this.isFromExtensionOrigin(event.origin.toLowerCase()) ||
|
||||
!message.portKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data;
|
||||
|
||||
if (this.port) {
|
||||
this.port.postMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.command !== "initAutofillOverlayPort") {
|
||||
return;
|
||||
if (message.command === "initAutofillOverlayPort") {
|
||||
this.port = chrome.runtime.connect({ name: message.portName });
|
||||
}
|
||||
|
||||
this.port = chrome.runtime.connect({ name: message.portName });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,7 @@ describe("AutofillOverlayPageElement", () => {
|
||||
"https://jest-testing-website.com",
|
||||
translations,
|
||||
"https://jest-testing-website.com/message-connector",
|
||||
"portKey",
|
||||
);
|
||||
|
||||
expect(globalThis.document.documentElement.setAttribute).toHaveBeenCalledWith(
|
||||
|
||||
@@ -11,6 +11,7 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
protected messageOrigin: string;
|
||||
protected translations: Record<string, string>;
|
||||
protected messageConnectorIframe: HTMLIFrameElement;
|
||||
private portKey: string;
|
||||
protected windowMessageHandlers: WindowMessageHandlers;
|
||||
|
||||
constructor() {
|
||||
@@ -27,13 +28,17 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
* @param styleSheetUrl - The URL of the stylesheet 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 portKey - Background generated key that allows the port to communicate with the background
|
||||
*/
|
||||
protected async initOverlayPage(
|
||||
elementName: "button" | "list",
|
||||
styleSheetUrl: string,
|
||||
translations: Record<string, string>,
|
||||
messageConnectorUrl: string,
|
||||
portKey: string,
|
||||
): Promise<HTMLLinkElement> {
|
||||
this.portKey = portKey;
|
||||
|
||||
this.translations = translations;
|
||||
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
||||
globalThis.document.head.title = this.getTranslation(`${elementName}PageTitle`);
|
||||
@@ -79,7 +84,10 @@ class AutofillOverlayPageElement extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageConnectorIframe.contentWindow.postMessage(message, "*");
|
||||
this.messageConnectorIframe.contentWindow.postMessage(
|
||||
{ portKey: this.portKey, ...message },
|
||||
"*",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,6 +174,7 @@ function createInitAutofillOverlayButtonMessageMock(
|
||||
styleSheetUrl: "https://jest-testing-website.com",
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
messageConnectorUrl: "https://jest-testing-website.com/message-connector",
|
||||
portKey: "portKey",
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
@@ -205,6 +206,7 @@ function createInitAutofillOverlayListMessageMock(
|
||||
theme: ThemeType.Light,
|
||||
authStatus: AuthenticationStatus.Unlocked,
|
||||
messageConnectorUrl: "https://jest-testing-website.com/message-connector",
|
||||
portKey: "portKey",
|
||||
ciphers: [
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
icon: {
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
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.
|
||||
*/
|
||||
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 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 {
|
||||
generateRandomChars,
|
||||
generateRandomCustomElementName,
|
||||
buildSvgDomElement,
|
||||
sendExtensionMessage,
|
||||
|
||||
Reference in New Issue
Block a user