mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
PM-26985 Use a Shadow DOM for the notification bar iframe to address FF fingerprinting issues (#16903)
* PM-26985 Use a Shadow DOM for the notification bar iframe to address FF fingerprinting issues * update tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body 1`] = `
|
||||
exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body within a shadow root 1`] = `
|
||||
<div
|
||||
id="bit-notification-bar"
|
||||
style="height: 400px !important; width: 430px !important; max-width: calc(100% - 20px) !important; min-height: initial !important; top: 10px !important; right: 0px !important; padding: 0px !important; position: fixed !important; z-index: 2147483647 !important; visibility: visible !important; border-radius: 4px !important; background-color: transparent !important; overflow: hidden !important; transition: box-shadow 0.15s ease !important; transition-delay: 0.15s;"
|
||||
|
||||
@@ -16,10 +16,13 @@ describe("OverlayNotificationsContentService", () => {
|
||||
let domElementVisibilityService: DomElementVisibilityService;
|
||||
let autofillInit: AutofillInit;
|
||||
let bodyAppendChildSpy: jest.SpyInstance;
|
||||
let postMessageSpy: jest.SpyInstance<void, Parameters<Window["postMessage"]>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn());
|
||||
jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
|
||||
postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
|
||||
domQueryService = mock<DomQueryService>();
|
||||
domElementVisibilityService = new DomElementVisibilityService();
|
||||
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
||||
@@ -48,7 +51,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
|
||||
it("closes the notification bar if the notification bar type has changed", async () => {
|
||||
overlayNotificationsContentService["currentNotificationBarType"] = "add";
|
||||
overlayNotificationsContentService["currentNotificationBarType"] = NotificationType.AddLogin;
|
||||
const closeNotificationBarSpy = jest.spyOn(
|
||||
overlayNotificationsContentService as any,
|
||||
"closeNotificationBar",
|
||||
@@ -66,7 +69,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
expect(closeNotificationBarSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates the notification bar elements and appends them to the body", async () => {
|
||||
it("creates the notification bar elements and appends them to the body within a shadow root", async () => {
|
||||
sendMockExtensionMessage({
|
||||
command: "openNotificationBar",
|
||||
data: {
|
||||
@@ -77,6 +80,13 @@ describe("OverlayNotificationsContentService", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayNotificationsContentService["notificationBarElement"]).toMatchSnapshot();
|
||||
|
||||
const rootElement = overlayNotificationsContentService["notificationBarRootElement"];
|
||||
expect(bodyAppendChildSpy).toHaveBeenCalledWith(rootElement);
|
||||
expect(rootElement?.tagName).toBe("BIT-NOTIFICATION-BAR-ROOT");
|
||||
|
||||
expect(document.getElementById("bit-notification-bar")).toBeNull();
|
||||
expect(document.querySelector("#bit-notification-bar-iframe")).toBeNull();
|
||||
});
|
||||
|
||||
it("sets up a slide in animation when the notification is fresh", async () => {
|
||||
@@ -116,6 +126,8 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
|
||||
it("sends an initialization message to the notification bar iframe", async () => {
|
||||
const addEventListenerSpy = jest.spyOn(globalThis, "addEventListener");
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "openNotificationBar",
|
||||
data: {
|
||||
@@ -124,10 +136,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
const postMessageSpy = jest.spyOn(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
|
||||
"postMessage",
|
||||
);
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
|
||||
globalThis.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
@@ -142,7 +151,6 @@ describe("OverlayNotificationsContentService", () => {
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(postMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
command: "initNotificationBar",
|
||||
@@ -158,7 +166,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
sendMockExtensionMessage({
|
||||
command: "openNotificationBar",
|
||||
data: {
|
||||
type: "change",
|
||||
type: NotificationType.ChangePassword,
|
||||
typeData: mock<NotificationTypeData>(),
|
||||
},
|
||||
});
|
||||
@@ -242,20 +250,15 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
|
||||
it("sends a message to the notification bar iframe indicating that the save attempt completed", () => {
|
||||
jest.spyOn(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
|
||||
"postMessage",
|
||||
);
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "saveCipherAttemptCompleted",
|
||||
data: { error: undefined },
|
||||
});
|
||||
|
||||
expect(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow
|
||||
.postMessage,
|
||||
).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: undefined }, "*");
|
||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||
{ command: "saveCipherAttemptCompleted", error: undefined },
|
||||
"*",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,9 +274,10 @@ describe("OverlayNotificationsContentService", () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("triggers a closure of the notification bar", () => {
|
||||
it("triggers a closure of the notification bar and cleans up all shadow DOM elements", () => {
|
||||
overlayNotificationsContentService.destroy();
|
||||
|
||||
expect(overlayNotificationsContentService["notificationBarRootElement"]).toBeNull();
|
||||
expect(overlayNotificationsContentService["notificationBarElement"]).toBeNull();
|
||||
expect(overlayNotificationsContentService["notificationBarIframeElement"]).toBeNull();
|
||||
});
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
export class OverlayNotificationsContentService
|
||||
implements OverlayNotificationsContentServiceInterface
|
||||
{
|
||||
private notificationBarRootElement: HTMLElement | null = null;
|
||||
private notificationBarElement: HTMLElement | null = null;
|
||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||
private notificationBarShadowRoot: ShadowRoot | null = null;
|
||||
private currentNotificationBarType: NotificationType | null = null;
|
||||
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||
height: "400px",
|
||||
@@ -158,12 +160,12 @@ export class OverlayNotificationsContentService
|
||||
* @private
|
||||
*/
|
||||
private openNotificationBar(initData: NotificationBarIframeInitData) {
|
||||
if (!this.notificationBarElement && !this.notificationBarIframeElement) {
|
||||
if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
|
||||
this.createNotificationBarIframeElement(initData);
|
||||
this.createNotificationBarElement();
|
||||
|
||||
this.setupInitNotificationBarMessageListener(initData);
|
||||
globalThis.document.body.appendChild(this.notificationBarElement);
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,15 +215,25 @@ export class OverlayNotificationsContentService
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the container for the notification bar iframe.
|
||||
* Creates the container for the notification bar iframe with shadow DOM.
|
||||
*/
|
||||
private createNotificationBarElement() {
|
||||
if (this.notificationBarIframeElement) {
|
||||
this.notificationBarRootElement = globalThis.document.createElement(
|
||||
"bit-notification-bar-root",
|
||||
);
|
||||
|
||||
this.notificationBarShadowRoot = this.notificationBarRootElement.attachShadow({
|
||||
mode: "closed",
|
||||
delegatesFocus: true,
|
||||
});
|
||||
|
||||
this.notificationBarElement = globalThis.document.createElement("div");
|
||||
this.notificationBarElement.id = "bit-notification-bar";
|
||||
|
||||
setElementStyles(this.notificationBarElement, this.notificationBarContainerStyles, true);
|
||||
|
||||
this.notificationBarShadowRoot.appendChild(this.notificationBarElement);
|
||||
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
|
||||
}
|
||||
}
|
||||
@@ -258,7 +270,7 @@ export class OverlayNotificationsContentService
|
||||
* @param closedByUserAction - Whether the notification bar was closed by the user.
|
||||
*/
|
||||
private closeNotificationBar(closedByUserAction: boolean = false) {
|
||||
if (!this.notificationBarElement && !this.notificationBarIframeElement) {
|
||||
if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,6 +279,9 @@ export class OverlayNotificationsContentService
|
||||
|
||||
this.notificationBarElement.remove();
|
||||
this.notificationBarElement = null;
|
||||
this.notificationBarShadowRoot = null;
|
||||
this.notificationBarRootElement.remove();
|
||||
this.notificationBarRootElement = null;
|
||||
|
||||
const removableNotificationTypes = new Set([
|
||||
NotificationTypes.Add,
|
||||
|
||||
Reference in New Issue
Block a user