1
0
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:
Daniel Riera
2025-10-24 10:35:55 -04:00
committed by GitHub
parent bc0e0f0781
commit 1da4fd2261
3 changed files with 42 additions and 23 deletions

View File

@@ -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;"

View File

@@ -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();
});

View File

@@ -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,