Medium CVSS 6.1 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.1
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionA missing delay in directory upload UI could have made it possible for an attacker to trick a user into granting permission via clickjacking.
ComponentDOM
Bug ClassLogic Error
Tracker1916659
Fix commitc67647610b53 (firefox) +61/-2
CWECWE-1021
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
CISA KEVNot listed
CreditedShaheen Fazim
Disclosed2024-10-01

Background

Clickjacking
an attack in which a malicious page overlays or times UI so a user’s click lands on a security-sensitive control they did not intend to activate.
Security delay
a period during which a confirmation dialog’s accept button stays disabled so that a click cannot be registered immediately after the dialog appears.
`BUTTON_DELAY_ENABLE`
an nsIPrompt button flag that tells the prompt service to keep the associated button disabled for the duration of security.dialog_enable_delay before enabling it.
Directory upload prompt
the tab-modal confirmation shown by PromptCollection when a web page requests to upload the contents of a directory.

Root Cause Analysis

When a web page triggered a directory (folder) upload, PromptCollection.confirmFolderUpload built a tab-modal confirmation prompt through Services.prompt and passed a buttonFlags value that did not include Ci.nsIPrompt.BUTTON_DELAY_ENABLE. Because that flag was absent, the “Upload” accept button was clickable the instant the dialog rendered, violating the invariant that a security-sensitive confirmation must not accept input until the user has had time to perceive it. An attacker could therefore position or time the dialog so a pre-placed or induced click was consumed by the accept button, granting directory upload permission without the user’s genuine intent.

The fix ORs Ci.nsIPrompt.BUTTON_DELAY_ENABLE into buttonFlags, so the accept button starts disabled and only becomes usable after security.dialog_enable_delay elapses, closing the click-timing window while leaving the default Cancel button immediately available.

Key insight
The single mistake was omitting the BUTTON_DELAY_ENABLE flag when constructing the folder-upload confirmation, leaving its accept button live from the moment it appeared; the fix restores the standard security delay by setting that flag so the button cannot be clicked until the enforced delay passes.

Attack Path

  1. Lure the victim The attacker gets a user to visit a page that can invoke a directory upload (for example via a webkitdirectory file input or the File System Access API).
  2. Trigger the prompt Script on the page programmatically initiates the folder upload, causing PromptCollection.confirmFolderUpload to display the tab-modal “Upload” confirmation.
  3. Overlay or time the click Because the accept button lacks BUTTON_DELAY_ENABLE, the attacker uses a decoy UI or a scripted/coerced click that lands on the “Upload” button the instant the dialog appears.
  4. Consume the permission The immediate click activates the accept button, so the browser grants the directory upload before the user recognizes what was confirmed.
  5. Exfiltrate directory contents With permission granted, the page reads and uploads the contents of the selected directory to attacker-controlled storage.

Impact Assessment

An attacker gains the ability to have a whole directory’s contents uploaded from the victim’s file system by tricking them into confirming a prompt they did not mean to accept, all within the context of a normal web content page and its tab-modal dialog. Exploitation requires the victim to visit attacker-controlled or attacker-influenced content and to be induced into an immediate click on the freshly shown dialog. Rated moderate, the exposure is limited to directories the user could otherwise choose to share and depends on successful clickjacking timing.

Changed Functions

FunctionChangeNotes
if
dom/filesystem/tests/script_promptHandler.js
modified
for
dom/filesystem/tests/script_promptHandler.js
modified

Files Changed

  • browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
  • browser/components/prompts/PromptCollection.sys.mjs
  • dom/filesystem/tests/script_promptHandler.js

Audit Directions

  • Missing `BUTTON_DELAY_ENABLE` on sensitive prompts
    Search other Services.prompt / nsIPrompt call sites that grant permissions or perform destructive actions and confirm each security-relevant accept button sets the delay flag.
  • Immediately actionable confirmation dialogs
    Review UI that appears in response to web-content-triggered events for any accept control that is clickable before the user can react, which invites clickjacking.
  • Consistency across permission surfaces
    Compare directory/file upload, download, and File System Access confirmations to ensure the same delay and default-Cancel protections are applied uniformly.
diff --git a/browser/components/prompts/PromptCollection.sys.mjs b/browser/components/prompts/PromptCollection.sys.mjs
index 85a7a10057a..ebb630fe152 100644
--- a/browser/components/prompts/PromptCollection.sys.mjs
+++ b/browser/components/prompts/PromptCollection.sys.mjs
@@ -195,7 +195,7 @@ export class PromptCollection {
         Services.prompt.MODAL_TYPE_TAB,
         title,
         message,
-        buttonFlags,
+        buttonFlags | Ci.nsIPrompt.BUTTON_DELAY_ENABLE,
         acceptLabel,
         null,
         null,
diff --git a/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
index a7e5d76fc40..d62edfd7ebb 100644
--- a/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
+++ b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
@@ -107,8 +107,29 @@ async function testUploadPrompt(confirmUpload) {
     // Wait for confirmation prompt
     let prompt = await promptPromise;
     ok(prompt, "Shown upload confirmation prompt");
+
     is(prompt.ui.button0.label, "Upload", "Accept button label");
+    ok(
+      prompt.ui.button0.disabled,
+      "Accept button should be disabled by the security delay initially."
+    );
+
     ok(prompt.ui.button1.hasAttribute("default"), "Cancel is default button");
+    ok(
+      !prompt.ui.button1.disabled,
+      "Cancel button should not be disabled by the security delay."
+    );
+
+    info("Wait for the security delay to pass.");
+    let delayTime = Services.prefs.getIntPref("security.dialog_enable_delay");
+    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+    await new Promise(resolve => setTimeout(resolve, delayTime + 100));
+
+    ok(
+      !prompt.ui.button0.disabled,
+      "Accept button should no longer be disabled."
+    );
+    ok(!prompt.ui.button1.disabled, "Cancel button should remain enabled.");
 
     // Close confirmation prompt
     await PromptTestUtils.handlePrompt(prompt, {
diff --git a/dom/filesystem/tests/script_promptHandler.js b/dom/filesystem/tests/script_promptHandler.js
index 235842b455d..9b412a85bb3 100644
--- a/dom/filesystem/tests/script_promptHandler.js
+++ b/dom/filesystem/tests/script_promptHandler.js
@@ -2,7 +2,45 @@
 
 let dialogObserverTopic = "common-dialog-loaded";
 
-function dialogObserver(subj) {
+function waitForButtonEnabledState(button) {
+  return new Promise(resolve => {
+    // Check if the button is already enabled (not disabled)
+    if (!button.disabled) {
+      resolve();
+      return;
+    }
+
+    // Create a MutationObserver instance
+    let win = button.ownerGlobal;
+    let { MutationObserver } = win;
+    const observer = new MutationObserver(mutationsList => {
+      for (const mutation of mutationsList) {
+        if (
+          mutation.type === "attributes" &&
+          mutation.attributeName === "disabled"
+        ) {
+          if (!button.disabled) {
+            // Resolve the promise when the button is enabled
+            observer.disconnect(); // Stop observing
+            resolve();
+          }
+        }
+      }
+    });
+
+    // Start observing the button for changes to the 'disabled' attribute
+    observer.observe(button, {
+      attributes: true,
+      attributeFilter: ["disabled"],
+    });
+  });
+}
+
+async function dialogObserver(subj) {
+  let dialog = subj.document.querySelector("dialog");
+  let acceptButton = dialog.getButton("accept");
+  await waitForButtonEnabledState(acceptButton);
+
   subj.document.querySelector("dialog").acceptDialog();
   sendAsyncMessage("promptAccepted");
 }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
index a7e5d76fc40..d62edfd7ebb 100644
--- a/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
+++ b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js
@@ -107,8 +107,29 @@ async function testUploadPrompt(confirmUpload) {
     // Wait for confirmation prompt
     let prompt = await promptPromise;
     ok(prompt, "Shown upload confirmation prompt");
+
     is(prompt.ui.button0.label, "Upload", "Accept button label");
+    ok(
+      prompt.ui.button0.disabled,
+      "Accept button should be disabled by the security delay initially."
+    );
+
     ok(prompt.ui.button1.hasAttribute("default"), "Cancel is default button");
+    ok(
+      !prompt.ui.button1.disabled,
+      "Cancel button should not be disabled by the security delay."
+    );
+
+    info("Wait for the security delay to pass.");
+    let delayTime = Services.prefs.getIntPref("security.dialog_enable_delay");
+    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+    await new Promise(resolve => setTimeout(resolve, delayTime + 100));
+
+    ok(
+      !prompt.ui.button0.disabled,
+      "Accept button should no longer be disabled."
+    );
+    ok(!prompt.ui.button1.disabled, "Cancel button should remain enabled.");
 
     // Close confirmation prompt
     await PromptTestUtils.handlePrompt(prompt, {
diff --git a/dom/filesystem/tests/script_promptHandler.js b/dom/filesystem/tests/script_promptHandler.js
index 235842b455d..9b412a85bb3 100644
--- a/dom/filesystem/tests/script_promptHandler.js
+++ b/dom/filesystem/tests/script_promptHandler.js
@@ -2,7 +2,45 @@
 
 let dialogObserverTopic = "common-dialog-loaded";
 
-function dialogObserver(subj) {
+function waitForButtonEnabledState(button) {
+  return new Promise(resolve => {
+    // Check if the button is already enabled (not disabled)
+    if (!button.disabled) {
+      resolve();
+      return;
+    }
+
+    // Create a MutationObserver instance
+    let win = button.ownerGlobal;
+    let { MutationObserver } = win;
+    const observer = new MutationObserver(mutationsList => {
+      for (const mutation of mutationsList) {
+        if (
+          mutation.type === "attributes" &&
+          mutation.attributeName === "disabled"
+        ) {
+          if (!button.disabled) {
+            // Resolve the promise when the button is enabled
+            observer.disconnect(); // Stop observing
+            resolve();
+          }
+        }
+      }
+    });
+
+    // Start observing the button for changes to the 'disabled' attribute
+    observer.observe(button, {
+      attributes: true,
+      attributeFilter: ["disabled"],
+    });
+  });
+}
+
+async function dialogObserver(subj) {
+  let dialog = subj.document.querySelector("dialog");
+  let acceptButton = dialog.getButton("accept");
+  await waitForButtonEnabledState(acceptButton);
+
   subj.document.querySelector("dialog").acceptDialog();
   sendAsyncMessage("promptAccepted");
 }
Loading diff…