1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-25025] Additional defense against top-layer content (#16101)

* additional defense against top-layer content

* fix error and update tests
This commit is contained in:
Jonathan Prusik
2025-08-22 11:09:00 -04:00
committed by Robyn MacCallum
parent afbe27591b
commit 4af4a863be
2 changed files with 54 additions and 23 deletions

View File

@@ -41,6 +41,10 @@ describe("AutofillInlineMenuContentService", () => {
autofillInlineMenuContentService as any, autofillInlineMenuContentService as any,
"sendExtensionMessage", "sendExtensionMessage",
); );
jest.spyOn(autofillInlineMenuContentService as any, "getPageIsOpaque");
jest
.spyOn(autofillInlineMenuContentService as any, "getPageTopLayerInUse")
.mockResolvedValue(false);
}); });
afterEach(() => { afterEach(() => {
@@ -386,30 +390,45 @@ describe("AutofillInlineMenuContentService", () => {
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled(); expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
}); });
it("closes the inline menu if the page has content in the top layer", async () => {
document.querySelector("html").style.opacity = "1";
document.body.style.opacity = "1";
jest
.spyOn(autofillInlineMenuContentService as any, "getPageTopLayerInUse")
.mockResolvedValue(true);
await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(true);
expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled();
});
it("closes the inline menu if the page body is not sufficiently opaque", async () => { it("closes the inline menu if the page body is not sufficiently opaque", async () => {
document.querySelector("html").style.opacity = "0.9"; document.querySelector("html").style.opacity = "0.9";
document.body.style.opacity = "0"; document.body.style.opacity = "0";
autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false); expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(false);
expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled();
}); });
it("closes the inline menu if the page html is not sufficiently opaque", async () => { it("closes the inline menu if the page html is not sufficiently opaque", async () => {
document.querySelector("html").style.opacity = "0.3"; document.querySelector("html").style.opacity = "0.3";
document.body.style.opacity = "0.7"; document.body.style.opacity = "0.7";
autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]); await autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]);
expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false); expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(false);
expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled(); expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled();
}); });
it("does not close the inline menu if the page html and body is sufficiently opaque", async () => { it("does not close the inline menu if the page html and body is sufficiently opaque", async () => {
document.querySelector("html").style.opacity = "0.9"; document.querySelector("html").style.opacity = "0.9";
document.body.style.opacity = "1"; document.body.style.opacity = "1";
autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]); await autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
await waitForIdleCallback();
expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(true); expect(autofillInlineMenuContentService["getPageIsOpaque"]).toHaveReturnedWith(true);
expect(autofillInlineMenuContentService["closeInlineMenu"]).not.toHaveBeenCalled(); expect(autofillInlineMenuContentService["closeInlineMenu"]).not.toHaveBeenCalled();
}); });

View File

@@ -33,7 +33,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
private listElement?: HTMLElement; private listElement?: HTMLElement;
private htmlMutationObserver: MutationObserver; private htmlMutationObserver: MutationObserver;
private bodyMutationObserver: MutationObserver; private bodyMutationObserver: MutationObserver;
private pageIsOpaque = true;
private inlineMenuElementsMutationObserver: MutationObserver; private inlineMenuElementsMutationObserver: MutationObserver;
private containerElementMutationObserver: MutationObserver; private containerElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0; private mutationObserverIterations = 0;
@@ -52,7 +51,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}; };
constructor() { constructor() {
this.checkPageOpacity();
this.setupMutationObserver(); this.setupMutationObserver();
} }
@@ -405,20 +403,35 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
}); });
}; };
private checkPageOpacity = () => { private checkPageRisks = async () => {
this.pageIsOpaque = this.getPageIsOpaque(); const pageIsOpaque = await this.getPageIsOpaque();
const pageTopLayerInUse = await this.getPageTopLayerInUse();
if (!this.pageIsOpaque) { const risksFound = !pageIsOpaque || pageTopLayerInUse;
if (risksFound) {
this.closeInlineMenu(); this.closeInlineMenu();
} }
return risksFound;
};
/*
* Checks for known risks at the page level
*/
private handlePageMutations = async (mutations: MutationRecord[]) => {
if (mutations.some(({ type }) => type === "attributes")) {
await this.checkPageRisks();
}
}; };
private handlePageMutations = (mutations: MutationRecord[]) => { /**
for (const mutation of mutations) { * Checks if the page top layer has content (will obscure/overlap the inline menu)
if (mutation.type === "attributes") { */
this.checkPageOpacity(); private getPageTopLayerInUse = () => {
} const pageHasOpenPopover = !!globalThis.document.querySelector(":popover-open");
}
return pageHasOpenPopover;
}; };
/** /**
@@ -427,7 +440,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
* of parents below the body. Assumes the target element will be a direct child of the page * of parents below the body. Assumes the target element will be a direct child of the page
* `body` (enforced elsewhere). * `body` (enforced elsewhere).
*/ */
private getPageIsOpaque() { private getPageIsOpaque = () => {
// These are computed style values, so we don't need to worry about non-float values // These are computed style values, so we don't need to worry about non-float values
// for `opacity`, here // for `opacity`, here
const htmlOpacity = globalThis.window.getComputedStyle( const htmlOpacity = globalThis.window.getComputedStyle(
@@ -441,17 +454,16 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
const opacityThreshold = 0.6; const opacityThreshold = 0.6;
return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold; return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold;
} };
/** /**
* Processes the mutation of the element that contains the inline menu. Will trigger when an * Processes the mutation of the element that contains the inline menu. Will trigger when an
* idle moment in the execution of the main thread is detected. * idle moment in the execution of the main thread is detected.
*/ */
private processContainerElementMutation = async (containerElement: HTMLElement) => { private processContainerElementMutation = async (containerElement: HTMLElement) => {
// If the computed opacity of the body and parent is not sufficiently opaque, tear // If the page contains risks, tear down and prevent building the inline menu experience.
// down and prevent building the inline menu experience. const pageRisksFound = await this.checkPageRisks();
this.checkPageOpacity(); if (pageRisksFound) {
if (!this.pageIsOpaque) {
return; return;
} }