CVE-2025-3035
Overview
Background
- GenAI chat shortcuts
- Firefox’s
browser/components/genai/GenAI.sys.mjsmodule 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
aiActionButtonthat, on hover, builds the “Ask chat” items and captures the current selection/dataso the chat prompt can be populated. - `addAskChatItems` / `data`
- The handler saves the latest selection as
aiActionButton.dataand 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.
Attack Path
- Victim interaction, tab A
The user selects text and uses the AI chat shortcut in one tab; a
mouseoverhandler capturing tab A’s context (including its document title) is attached to the sharedaiActionButton. - 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.
- Handler re-fire Hovering the button fires all attached handlers; the stale tab-A handler still runs because it was never removed.
- 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).
- Primitive Cross-tab information disclosure: one document’s title is exposed in another tab’s AI chat prompt without user intent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
add_taskbrowser/components/genai/tests/browser/browser_chat_shortcuts.js |
modified |
Files Changed
browser/components/genai/GenAI.sys.mjsbrowser/components/genai/tests/browser/browser_chat_shortcuts.js
Audit Directions
- Listeners on persistent chrome elementsAudit other GenAI/sidebar code (and chrome UI generally) that attaches
addEventListenerto reused, window-scoped elements without a matchingremoveEventListener, which produces the same accumulation. - Per-tab context captured in closuresReview 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 provenanceCheck where
aiActionButton.dataand document metadata (title, URL) are read when building chat prompts to ensure they always reflect the currently targeted browser.
Patch
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
*/
Regression Test / PoC
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
*/