mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 22:44:11 +00:00
PM-28831 Add isTrusted checks to ignore programmatically generated events (#18627)
* ignore events that do not originate from the user agent * [pm-28831] Add isTrusted checks and update tests * [pm-28831] Add isTrusted check to click events * [pm-28831] Replace in-code jest exceptions with new utils * [pm-28831] Move isTrusted checks to testable util * [pm-28831] Remove redundant check in cipher-action.ts * [pm-28831] Add isTrusted checks to click events in autofill-inine-menu-list --------- Signed-off-by: Ben Brooks <bbrooks@bitwarden.com> Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { html, TemplateResult } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../utils/event-security";
|
||||
import { border, themes, typography, spacing } from "../constants/styles";
|
||||
import { Spinner } from "../icons";
|
||||
|
||||
@@ -26,7 +27,7 @@ export function ActionButton({
|
||||
fullWidth = true,
|
||||
}: ActionButtonProps) {
|
||||
const handleButtonClick = (event: Event) => {
|
||||
if (!disabled && !isLoading) {
|
||||
if (EventSecurity.isEventTrusted(event) && !disabled && !isLoading) {
|
||||
handleClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../utils/event-security";
|
||||
import { border, themes, typography, spacing } from "../constants/styles";
|
||||
|
||||
export type BadgeButtonProps = {
|
||||
@@ -23,7 +24,7 @@ export function BadgeButton({
|
||||
username,
|
||||
}: BadgeButtonProps) {
|
||||
const handleButtonClick = (event: Event) => {
|
||||
if (!disabled) {
|
||||
if (EventSecurity.isEventTrusted(event) && !disabled) {
|
||||
buttonAction(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../utils/event-security";
|
||||
import { themes, typography, spacing } from "../constants/styles";
|
||||
import { PencilSquare } from "../icons";
|
||||
|
||||
@@ -21,7 +22,7 @@ export function EditButton({ buttonAction, buttonText, disabled = false, theme }
|
||||
aria-label=${buttonText}
|
||||
class=${editButtonStyles({ disabled, theme })}
|
||||
@click=${(event: Event) => {
|
||||
if (!disabled) {
|
||||
if (EventSecurity.isEventTrusted(event) && !disabled) {
|
||||
buttonAction(event);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html, nothing } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../../utils/event-security";
|
||||
import { spacing, themes, typography } from "../../constants/styles";
|
||||
|
||||
export type NotificationConfirmationMessageProps = {
|
||||
@@ -127,7 +128,7 @@ const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css`
|
||||
`;
|
||||
|
||||
function handleButtonKeyDown(event: KeyboardEvent, handleClick: () => void) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
if (EventSecurity.isEventTrusted(event) && (event.key === "Enter" || event.key === " ")) {
|
||||
event.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html, nothing } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../utils/event-security";
|
||||
import { IconProps, Option } from "../common-types";
|
||||
import { themes, spacing } from "../constants/styles";
|
||||
|
||||
@@ -29,6 +30,13 @@ export function OptionItem({
|
||||
handleSelection,
|
||||
}: OptionItemProps) {
|
||||
const handleSelectionKeyUpProxy = (event: KeyboardEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listenedForKeys = new Set(["Enter", "Space"]);
|
||||
if (listenedForKeys.has(event.code) && event.target instanceof Element) {
|
||||
handleSelection();
|
||||
@@ -37,6 +45,17 @@ export function OptionItem({
|
||||
return;
|
||||
};
|
||||
|
||||
const handleSelectionClickProxy = (event: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSelection();
|
||||
};
|
||||
|
||||
const iconProps: IconProps = { color: themes[theme].text.main, theme };
|
||||
const itemIcon = icon?.(iconProps);
|
||||
const ariaLabel =
|
||||
@@ -52,7 +71,7 @@ export function OptionItem({
|
||||
title=${text}
|
||||
role="option"
|
||||
aria-label=${ariaLabel}
|
||||
@click=${handleSelection}
|
||||
@click=${handleSelectionClickProxy}
|
||||
@keyup=${handleSelectionKeyUpProxy}
|
||||
>
|
||||
${itemIcon ? html`<div class=${optionItemIconContainerStyles}>${itemIcon}</div>` : nothing}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html, nothing } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../utils/event-security";
|
||||
import { Option } from "../common-types";
|
||||
import { themes, typography, scrollbarStyles, spacing } from "../constants/styles";
|
||||
|
||||
@@ -57,6 +58,10 @@ export function OptionItems({
|
||||
}
|
||||
|
||||
function handleMenuKeyUp(event: KeyboardEvent) {
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [
|
||||
...(event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>('[tabindex="0"]'),
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ import { property, state } from "lit/decorators.js";
|
||||
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { EventSecurity } from "../../../utils/event-security";
|
||||
import { OptionSelectionButton } from "../buttons/option-selection-button";
|
||||
import { Option } from "../common-types";
|
||||
|
||||
@@ -54,7 +55,7 @@ export class OptionSelection extends LitElement {
|
||||
private static currentOpenInstance: OptionSelection | null = null;
|
||||
|
||||
private handleButtonClick = async (event: Event) => {
|
||||
if (!this.disabled) {
|
||||
if (EventSecurity.isEventTrusted(event) && !this.disabled) {
|
||||
const isOpening = !this.showMenu;
|
||||
|
||||
if (isOpening) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
|
||||
import { postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||
import { EventSecurity } from "../utils/event-security";
|
||||
|
||||
describe("ContentMessageHandler", () => {
|
||||
const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage");
|
||||
@@ -19,6 +20,7 @@ describe("ContentMessageHandler", () => {
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./content-message-handler");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ExtensionPageUrls } from "@bitwarden/common/vault/enums";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
|
||||
import { EventSecurity } from "../utils/event-security";
|
||||
|
||||
import {
|
||||
ContentMessageWindowData,
|
||||
ContentMessageWindowEventHandlers,
|
||||
@@ -92,7 +94,10 @@ function handleOpenBrowserExtensionToUrlMessage({ url }: { url?: ExtensionPageUr
|
||||
*/
|
||||
function handleWindowMessageEvent(event: MessageEvent) {
|
||||
const { source, data, origin } = event;
|
||||
if (source !== window || !data?.command) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event) || source !== window || !data?.command) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { EventSecurity } from "../utils/event-security";
|
||||
|
||||
const inputTags = ["input", "textarea", "select"];
|
||||
const labelTags = ["label", "span"];
|
||||
const attributeKeys = ["id", "name", "label-aria", "placeholder"];
|
||||
@@ -52,6 +54,12 @@ function isNullOrEmpty(s: string | null) {
|
||||
// We only have access to the element that's been clicked when the context menu is first opened.
|
||||
// Remember it for use later.
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
clickedElement = event.target as HTMLElement;
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
createInitAutofillInlineMenuListMessageMock,
|
||||
} from "../../../../spec/autofill-mocks";
|
||||
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
||||
import { EventSecurity } from "../../../../utils/event-security";
|
||||
|
||||
import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
|
||||
|
||||
@@ -28,6 +29,7 @@ describe("AutofillInlineMenuList", () => {
|
||||
const events: { eventName: any; callback: any }[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
|
||||
const oldEv = globalThis.addEventListener;
|
||||
globalThis.addEventListener = (eventName: any, callback: any) => {
|
||||
events.push({ eventName, callback });
|
||||
|
||||
@@ -10,6 +10,7 @@ import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||
import { InlineMenuFillType } from "../../../../enums/autofill-overlay.enum";
|
||||
import { buildSvgDomElement, specialCharacterToKeyMap, throttle } from "../../../../utils";
|
||||
import { EventSecurity } from "../../../../utils/event-security";
|
||||
import {
|
||||
creditCardIcon,
|
||||
globeIcon,
|
||||
@@ -203,7 +204,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
|
||||
private handleSaveLoginInlineMenuKeyUp = (event: KeyboardEvent) => {
|
||||
const listenedForKeys = new Set(["ArrowDown"]);
|
||||
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
|
||||
if (
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
!listenedForKeys.has(event.code) ||
|
||||
!(event.target instanceof Element)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,7 +237,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* Handles the click event for the unlock button.
|
||||
* Sends a message to the parent window to unlock the vault.
|
||||
*/
|
||||
private handleUnlockButtonClick = () => {
|
||||
private handleUnlockButtonClick = (event: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessageToParent({ command: "unlockVault" });
|
||||
};
|
||||
|
||||
@@ -352,7 +367,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* Handles the click event for the fill generated password button. Triggers
|
||||
* a message to the background script to fill the generated password.
|
||||
*/
|
||||
private handleFillGeneratedPasswordClick = () => {
|
||||
private handleFillGeneratedPasswordClick = (event?: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (event && !EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessageToParent({ command: "fillGeneratedPassword" });
|
||||
};
|
||||
|
||||
@@ -362,7 +384,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* @param event - The keyup event.
|
||||
*/
|
||||
private handleFillGeneratedPasswordKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
event.ctrlKey ||
|
||||
event.altKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -388,6 +419,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* @param event - The click event.
|
||||
*/
|
||||
private handleRefreshGeneratedPasswordClick = (event?: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (event && !EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event) {
|
||||
(event.target as HTMLElement)
|
||||
.closest(".password-generator-actions")
|
||||
@@ -403,7 +441,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* @param event - The keyup event.
|
||||
*/
|
||||
private handleRefreshGeneratedPasswordKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
event.ctrlKey ||
|
||||
event.altKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -620,7 +667,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* Handles the click event for the new item button.
|
||||
* Sends a message to the parent window to add a new vault item.
|
||||
*/
|
||||
private handleNewLoginVaultItemAction = () => {
|
||||
private handleNewLoginVaultItemAction = (event: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let addNewCipherType = this.inlineMenuFillType;
|
||||
|
||||
if (this.showInlineMenuAccountCreation) {
|
||||
@@ -958,7 +1012,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => {
|
||||
const usePasskey = !!cipher.login?.passkey;
|
||||
return this.useEventHandlersMemo(
|
||||
() => this.triggerFillCipherClickEvent(cipher, usePasskey),
|
||||
(event: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.triggerFillCipherClickEvent(cipher, usePasskey);
|
||||
},
|
||||
`${cipher.id}-fill-cipher-button-click-handler-${usePasskey ? "passkey" : ""}`,
|
||||
);
|
||||
};
|
||||
@@ -990,7 +1053,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
*/
|
||||
private handleFillCipherKeyUpEvent = (event: KeyboardEvent) => {
|
||||
const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowRight"]);
|
||||
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
!listenedForKeys.has(event.code) ||
|
||||
!(event.target instanceof Element)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1018,7 +1088,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
*/
|
||||
private handleNewItemButtonKeyUpEvent = (event: KeyboardEvent) => {
|
||||
const listenedForKeys = new Set(["ArrowDown", "ArrowUp"]);
|
||||
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
!listenedForKeys.has(event.code) ||
|
||||
!(event.target instanceof Element)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1063,11 +1140,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* @param cipher - The cipher to view.
|
||||
*/
|
||||
private handleViewCipherClickEvent = (cipher: InlineMenuCipherData) => {
|
||||
return this.useEventHandlersMemo(
|
||||
() =>
|
||||
this.postMessageToParent({ command: "viewSelectedCipher", inlineMenuCipherId: cipher.id }),
|
||||
`${cipher.id}-view-cipher-button-click-handler`,
|
||||
);
|
||||
return this.useEventHandlersMemo((event: MouseEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessageToParent({ command: "viewSelectedCipher", inlineMenuCipherId: cipher.id });
|
||||
}, `${cipher.id}-view-cipher-button-click-handler`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1080,7 +1162,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
*/
|
||||
private handleViewCipherKeyUpEvent = (event: KeyboardEvent) => {
|
||||
const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowLeft"]);
|
||||
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
!listenedForKeys.has(event.code) ||
|
||||
!(event.target instanceof Element)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { RedirectFocusDirection } from "../../../../enums/autofill-overlay.enum";
|
||||
import { EventSecurity } from "../../../../utils/event-security";
|
||||
import {
|
||||
AutofillInlineMenuPageElementWindowMessage,
|
||||
AutofillInlineMenuPageElementWindowMessageHandlers,
|
||||
@@ -163,7 +164,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
||||
*/
|
||||
private handleDocumentKeyDownEvent = (event: KeyboardEvent) => {
|
||||
const listenedForKeys = new Set(["Tab", "Escape", "ArrowUp", "ArrowDown"]);
|
||||
if (!listenedForKeys.has(event.code)) {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event) || !listenedForKeys.has(event.code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
sendMockExtensionMessage,
|
||||
} from "../spec/testing-utils";
|
||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||
import { EventSecurity } from "../utils/event-security";
|
||||
|
||||
import { AutoFillConstants } from "./autofill-constants";
|
||||
import { AutofillOverlayContentService } from "./autofill-overlay-content.service";
|
||||
@@ -55,6 +56,9 @@ describe("AutofillOverlayContentService", () => {
|
||||
const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock EventSecurity to allow synthetic events in tests
|
||||
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
|
||||
|
||||
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||
domQueryService = new DomQueryService();
|
||||
domElementVisibilityService = new DomElementVisibilityService();
|
||||
@@ -331,6 +335,8 @@ describe("AutofillOverlayContentService", () => {
|
||||
pageDetailsMock,
|
||||
);
|
||||
jest.spyOn(globalThis.customElements, "define").mockImplementation();
|
||||
// Mock EventSecurity to allow synthetic events in tests
|
||||
jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("closes the autofill inline menu when the `Escape` key is pressed", () => {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
sendExtensionMessage,
|
||||
throttle,
|
||||
} from "../utils";
|
||||
import { EventSecurity } from "../utils/event-security";
|
||||
|
||||
import {
|
||||
AutofillOverlayContentExtensionMessageHandlers,
|
||||
@@ -618,6 +619,10 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
*/
|
||||
private handleSubmitButtonInteraction = (event: PointerEvent) => {
|
||||
if (
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
!EventSecurity.isEventTrusted(event) ||
|
||||
!this.submitElements.has(event.target as HTMLElement) ||
|
||||
(event.type === "keyup" &&
|
||||
!["Enter", "Space"].includes((event as unknown as KeyboardEvent).code))
|
||||
@@ -703,6 +708,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
* @param event - The keyup event.
|
||||
*/
|
||||
private handleFormFieldKeyupEvent = async (event: globalThis.KeyboardEvent) => {
|
||||
/**
|
||||
* Reject synthetic events (not originating from the user agent)
|
||||
*/
|
||||
if (!EventSecurity.isEventTrusted(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventCode = event.code;
|
||||
if (eventCode === "Escape") {
|
||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||
|
||||
26
apps/browser/src/autofill/utils/event-security.spec.ts
Normal file
26
apps/browser/src/autofill/utils/event-security.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { EventSecurity } from "./event-security";
|
||||
|
||||
describe("EventSecurity", () => {
|
||||
describe("isEventTrusted", () => {
|
||||
it("should call the event.isTrusted property", () => {
|
||||
const testEvent = new KeyboardEvent("keyup", { code: "Escape" });
|
||||
const result = EventSecurity.isEventTrusted(testEvent);
|
||||
|
||||
// In test environment, events are untrusted by default
|
||||
expect(result).toBe(false);
|
||||
expect(result).toBe(testEvent.isTrusted);
|
||||
});
|
||||
|
||||
it("should be mockable with jest.spyOn", () => {
|
||||
const testEvent = new KeyboardEvent("keyup", { code: "Escape" });
|
||||
const spy = jest.spyOn(EventSecurity, "isEventTrusted").mockReturnValue(true);
|
||||
|
||||
const result = EventSecurity.isEventTrusted(testEvent);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(spy).toHaveBeenCalledWith(testEvent);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
13
apps/browser/src/autofill/utils/event-security.ts
Normal file
13
apps/browser/src/autofill/utils/event-security.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Event security utilities for validating trusted events
|
||||
*/
|
||||
export class EventSecurity {
|
||||
/**
|
||||
* Validates that an event is trusted (originated from user agent)
|
||||
* @param event - The event to validate
|
||||
* @returns true if the event is trusted, false otherwise
|
||||
*/
|
||||
static isEventTrusted(event: Event): boolean {
|
||||
return event.isTrusted;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user