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:
committed by
Robyn MacCallum
parent
afbe27591b
commit
4af4a863be
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user