Medium CVSS 5.3 firefox Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionBy first using the AI chatbot in one tab and later activating it in another tab, the document title of the previous tab would leak into the chat prompt.
ComponentCore
Bug ClassInfo Disclosure
Tracker1952268
Fix commit7b9c9c970ef4 (firefox) +43/-2
CWECWE-359
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedMatthew Noorenberghe
Disclosed2025-04-01

Background

GenAI chat shortcuts
Firefox’s browser/components/genai/GenAI.sys.mjs module builds the floating “AI action” button and shortcut panels that appear over selected text, then forwards the selection (and page context) into a sidebar chat prompt.
`aiActionButton`
The DOM button element that the shortcut UI is attached to. It is a long-lived, per-window chrome element reused across tabs, not recreated each time a selection is made.
`mouseover` listener
A handler attached to aiActionButton that, on hover, builds the “Ask chat” items and captures the current selection/data so the chat prompt can be populated.
`addAskChatItems` / `data`
The handler saves the latest selection as aiActionButton.data and later assembles the chat prompt, which (per the advisory) includes the originating document’s title.

Root Cause Analysis

The shortcut code registered its hover handler with aiActionButton.addEventListener("mouseover", async () => { ... }) every time shortcuts were shown for a new selection, but it never removed the previously registered handler. Because aiActionButton is a single persistent element shared by all tabs in a window, each activation stacked another closure on top of the old ones. Each closure captured the selection context (data, browser/title) from the tab that was active when it was registered. When the user later triggered the button in a different tab, every still-attached listener fired — including the stale one from the first tab — so the earlier tab’s captured context (its document title) leaked into the chat prompt built for the second tab.

The patch converts the anonymous handler into a named reference stored on the element (aiActionButton.listener = async () => { ... }; aiActionButton.addEventListener("mouseover", aiActionButton.listener);) and adds a teardown in the hide() path that removes it (aiActionButton.removeEventListener("mouseover", aiActionButton.listener); aiActionButton.listener = null;). This guarantees exactly one live handler bound to the current tab’s context at a time, so a subsequent activation in another tab no longer re-runs a closure that still references the previous tab’s document. The added browser_chat_shortcuts.js test (test_show_shortcuts_second_tab) asserts that when shortcuts are shown on a second tab, addAskChatItems is called exactly once and with the correct browser, confirming the stale-listener accumulation was the defect.

Key insight
A hover handler holding per-tab selection context was added on every activation but never removed, so a shared, persistent chrome button accumulated stale listeners that fired for the wrong tab and leaked its document title.

Attack Path

  1. Victim interaction, tab A The user selects text and uses the AI chat shortcut in one tab; a mouseover handler capturing tab A’s context (including its document title) is attached to the shared aiActionButton.
  2. Context switch The user moves to tab B (which may host attacker-controlled or sensitive content) and invokes the AI chat shortcut there, attaching a second handler.
  3. Handler re-fire Hovering the button fires all attached handlers; the stale tab-A handler still runs because it was never removed.
  4. Leak into prompt Tab A’s captured document title is injected into the chat prompt generated in tab B’s session (inferred exact field from the advisory; the diff proves only the stale-listener mechanism).
  5. Primitive Cross-tab information disclosure: one document’s title is exposed in another tab’s AI chat prompt without user intent.

Impact Assessment

This is a privacy/information-disclosure defect in privileged chrome JavaScript (parent process) affecting the GenAI feature, requiring no special privilege beyond normal interactive use of the chatbot in two tabs. The leaked primitive is limited to the previous tab’s document title appearing in a later tab’s chat prompt (CVSS confidentiality-only, 5.3), not memory corruption or code execution. It is gated on the user having enabled and used the AI chat shortcut feature and on the ordering of activations across tabs.

Changed Functions

FunctionChangeNotes
add_task
browser/components/genai/tests/browser/browser_chat_shortcuts.js
modified

Files Changed

  • browser/components/genai/GenAI.sys.mjs
  • browser/components/genai/tests/browser/browser_chat_shortcuts.js

Audit Directions

  • Listeners on persistent chrome elements
    Audit other GenAI/sidebar code (and chrome UI generally) that attaches addEventListener to reused, window-scoped elements without a matching removeEventListener, which produces the same accumulation.
  • Per-tab context captured in closures
    Review any handler that closes over tab/browser/selection state attached to shared UI; such closures can execute with the wrong tab’s data after a tab switch.
  • Prompt assembly provenance
    Check where aiActionButton.data and document metadata (title, URL) are read when building chat prompts to ensure they always reflect the currently targeted browser.
diff --git a/browser/components/genai/GenAI.sys.mjs b/browser/components/genai/GenAI.sys.mjs
index ab4b461aab5..2cd404595e7 100644
--- a/browser/components/genai/GenAI.sys.mjs
+++ b/browser/components/genai/GenAI.sys.mjs
@@ -435,6 +435,8 @@ export const GenAI = {
     // Hide shortcuts and panel
     const hide = () => {
       aiActionButton.setAttribute("type", buttonDefaultState);
+      aiActionButton.removeEventListener("mouseover", aiActionButton.listener);
+      aiActionButton.listener = null;
       chatShortcutsOptionsPanel.hidePopup();
       selectionShortcutActionPanel.hidePopup();
     };
@@ -486,7 +488,7 @@ export const GenAI = {
         aiActionButton.setAttribute("type", buttonDefaultState);
 
         // Detect hover to build and open the popup
-        aiActionButton.addEventListener("mouseover", async () => {
+        aiActionButton.listener = async () => {
           if (aiActionButton.hasAttribute("active")) {
             return;
           }
@@ -592,7 +594,8 @@ export const GenAI = {
             provider: this.getProviderId(),
             warning: showWarning,
           });
-        });
+        };
+        aiActionButton.addEventListener("mouseover", aiActionButton.listener);
 
         // Save the latest selection so it can be used by popup
         aiActionButton.data = data;
diff --git a/browser/components/genai/tests/browser/browser_chat_shortcuts.js b/browser/components/genai/tests/browser/browser_chat_shortcuts.js
index eb7f6028685..0fb240bc56d 100644
--- a/browser/components/genai/tests/browser/browser_chat_shortcuts.js
+++ b/browser/components/genai/tests/browser/browser_chat_shortcuts.js
@@ -107,6 +107,44 @@ add_task(async function test_show_shortcuts() {
   });
 });
 
+/**
+ * Check that shortcuts shown on second tab
+ */
+add_task(async function test_show_shortcuts_second_tab() {
+  // NB: this test runs after test_show_shortcuts, which showed shortcuts
+  await BrowserTestUtils.withNewTab(
+    "data:text/html,<title>second</title>page",
+    async browser => {
+      await SimpleTest.promiseFocus(browser);
+      const selectPromise = SpecialPowers.spawn(browser, [], () => {
+        ContentTaskUtils.waitForCondition(() => content.getSelection());
+      });
+      goDoCommand("cmd_selectAll");
+      await selectPromise;
+      BrowserTestUtils.synthesizeMouseAtCenter(
+        browser,
+        { type: "mouseup" },
+        browser
+      );
+
+      await TestUtils.waitForCondition(() => {
+        const panelElement = document.getElementById(
+          "selection-shortcut-action-panel"
+        );
+        return panelElement.getAttribute("panelopen") === "true";
+      });
+      const sandbox = sinon.createSandbox();
+      const stub = sandbox.stub(GenAI, "addAskChatItems");
+
+      const shortcuts = document.querySelector("#ai-action-button");
+      EventUtils.sendMouseEvent({ type: "mouseover" }, shortcuts);
+
+      Assert.equal(stub.callCount, 1, "Shortcuts added on select");
+      Assert.equal(stub.firstCall.args[0], browser, "Got correct browser");
+    }
+  );
+});
+
 /**
  * Check that the warning label is shown when too much text is selected
  */
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/components/genai/tests/browser/browser_chat_shortcuts.js b/browser/components/genai/tests/browser/browser_chat_shortcuts.js
index eb7f6028685..0fb240bc56d 100644
--- a/browser/components/genai/tests/browser/browser_chat_shortcuts.js
+++ b/browser/components/genai/tests/browser/browser_chat_shortcuts.js
@@ -107,6 +107,44 @@ add_task(async function test_show_shortcuts() {
   });
 });
 
+/**
+ * Check that shortcuts shown on second tab
+ */
+add_task(async function test_show_shortcuts_second_tab() {
+  // NB: this test runs after test_show_shortcuts, which showed shortcuts
+  await BrowserTestUtils.withNewTab(
+    "data:text/html,<title>second</title>page",
+    async browser => {
+      await SimpleTest.promiseFocus(browser);
+      const selectPromise = SpecialPowers.spawn(browser, [], () => {
+        ContentTaskUtils.waitForCondition(() => content.getSelection());
+      });
+      goDoCommand("cmd_selectAll");
+      await selectPromise;
+      BrowserTestUtils.synthesizeMouseAtCenter(
+        browser,
+        { type: "mouseup" },
+        browser
+      );
+
+      await TestUtils.waitForCondition(() => {
+        const panelElement = document.getElementById(
+          "selection-shortcut-action-panel"
+        );
+        return panelElement.getAttribute("panelopen") === "true";
+      });
+      const sandbox = sinon.createSandbox();
+      const stub = sandbox.stub(GenAI, "addAskChatItems");
+
+      const shortcuts = document.querySelector("#ai-action-button");
+      EventUtils.sendMouseEvent({ type: "mouseover" }, shortcuts);
+
+      Assert.equal(stub.callCount, 1, "Shortcuts added on select");
+      Assert.equal(stub.firstCall.args[0], browser, "Got correct browser");
+    }
+  );
+});
+
 /**
  * Check that the warning label is shown when too much text is selected
  */
Loading diff…