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 = {
|
type OverlayBackgroundExtensionMessage = {
|
||||||
command: string;
|
command: string;
|
||||||
|
portKey?: string;
|
||||||
tab?: chrome.tabs.Tab;
|
tab?: chrome.tabs.Tab;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
details?: AutofillPageDetails;
|
details?: AutofillPageDetails;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user