From a2d2788c95e9307de1ad2c14d3d4163c29df015d Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Thu, 16 Oct 2025 15:14:30 -0400 Subject: [PATCH 1/2] PM-26985 Use a Shadow DOM for the notification bar iframe to address FF fingerprinting issues --- .../overlay-notifications-content.service.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts index 4e09c3186bb2..0afa4f1409bd 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts @@ -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 = { 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, From bc477880a114123fbc8c6d07821aa391514051b3 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 17 Oct 2025 11:14:30 -0400 Subject: [PATCH 2/2] update tests --- ...notifications-content.service.spec.ts.snap | 2 +- ...rlay-notifications-content.service.spec.ts | 40 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap index e5bafe34b5fe..39ca68d912cb 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap @@ -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`] = `
{ let domElementVisibilityService: DomElementVisibilityService; let autofillInit: AutofillInit; let bodyAppendChildSpy: jest.SpyInstance; + let postMessageSpy: jest.SpyInstance>; 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(); 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(), }, }); @@ -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(); });