diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index 73fc1e79ec5..e83f2b4b77c 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -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); } }; diff --git a/apps/browser/src/autofill/content/components/buttons/badge-button.ts b/apps/browser/src/autofill/content/components/buttons/badge-button.ts index 3cdd453ee1a..98968d0b57b 100644 --- a/apps/browser/src/autofill/content/components/buttons/badge-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/badge-button.ts @@ -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); } }; diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts index ecbb736bb8e..88caae13590 100644 --- a/apps/browser/src/autofill/content/components/buttons/edit-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -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); } }} diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts index 36ea9c1f9d6..480b2acd0dd 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -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(); } diff --git a/apps/browser/src/autofill/content/components/option-selection/option-item.ts b/apps/browser/src/autofill/content/components/option-selection/option-item.ts index 6af6a2d6538..1cbabcb4f85 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-item.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-item.ts @@ -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`
${itemIcon}
` : nothing} diff --git a/apps/browser/src/autofill/content/components/option-selection/option-items.ts b/apps/browser/src/autofill/content/components/option-selection/option-items.ts index 58216b6c1b2..4c24a2fde8b 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-items.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-items.ts @@ -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('[tabindex="0"]'), ]; diff --git a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts index ee711456e9c..78c7d9f0646 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts @@ -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) { diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts index 874e1cc76ff..fb17874b0b7 100644 --- a/apps/browser/src/autofill/content/content-message-handler.spec.ts +++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts @@ -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"); diff --git a/apps/browser/src/autofill/content/content-message-handler.ts b/apps/browser/src/autofill/content/content-message-handler.ts index 63afc215923..874e760c4f8 100644 --- a/apps/browser/src/autofill/content/content-message-handler.ts +++ b/apps/browser/src/autofill/content/content-message-handler.ts @@ -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; } diff --git a/apps/browser/src/autofill/content/context-menu-handler.ts b/apps/browser/src/autofill/content/context-menu-handler.ts index d3926d57c9a..919ab5f1a3d 100644 --- a/apps/browser/src/autofill/content/context-menu-handler.ts +++ b/apps/browser/src/autofill/content/context-menu-handler.ts @@ -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; }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index 1e99ac9df90..212fe6d8c89 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -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 }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index c13c523e30a..39b7fa5c17b 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -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; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts index 5df6e7cd190..e7f99b28ecc 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts @@ -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; } diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 0fb031b52e8..c9a522c6b8c 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -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", () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 7ea89e114ab..eb02d05d671 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -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", { diff --git a/apps/browser/src/autofill/utils/event-security.spec.ts b/apps/browser/src/autofill/utils/event-security.spec.ts new file mode 100644 index 00000000000..5cda484d4d2 --- /dev/null +++ b/apps/browser/src/autofill/utils/event-security.spec.ts @@ -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(); + }); + }); +}); diff --git a/apps/browser/src/autofill/utils/event-security.ts b/apps/browser/src/autofill/utils/event-security.ts new file mode 100644 index 00000000000..e53517058df --- /dev/null +++ b/apps/browser/src/autofill/utils/event-security.ts @@ -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; + } +}