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;
+ }
+}