Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Blink
DescriptionIncorrect security UI in Blink
ComponentBlink
Bug ClassLogic Error
Tracker365089001
Fix commit9a7e491a5c91 (chromium/src) +86/-1
CISA KEVNot listed
CreditedShaheen Fazim
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
if
content/browser/web_contents/web_contents_impl.cc
modified
SelectPickerBrowserTest
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
SelectPickerBrowserTest
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
WebContentsImplInsecureLocalhostBrowserTest
content/browser/web_contents/web_contents_impl_browsertest.cc
modified
CommandLine
content/common/content_switches_internal.h
modified
if
content/public/test/browser_test_base.cc
modified

Files Changed

  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/web_contents/web_contents_impl.h
  • content/browser/web_contents/web_contents_impl_browsertest.cc
  • content/common/content_switches_internal.cc
  • content/common/content_switches_internal.h
  • content/public/test/browser_test_base.cc
  • content/renderer/render_thread_impl.cc
From 9a7e491a5c91e9a42b78e2ac3fc444c534ba2456 Mon Sep 17 00:00:00 2001
From: Mason Freed <[email protected]>
Date: Mon, 04 Nov 2024 17:33:17 +0000
Subject: [PATCH] Don't show popups (e.g. <select>) if the tab isn't focused

It was previously possible for pickers like <select>'s picker to be
shown on top of the not-currently-focused tab, confusing the user.
With this change, the select picker must be the currently-focused
tab for the picker to be opened. This is akin to existing protections
for the tab being visible.

Fixed: 365089001
Change-Id: Id2b15f5d310cce877341d7e9d5a5c5d6da882887
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5909884
Reviewed-by: Alexander Timin <[email protected]>
Commit-Queue: Mason Freed <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1377731}
---

diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index a5a0da2..9fba923 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -5037,6 +5037,13 @@
     // Don't create popups for hidden tabs. http://crbug.com/1521345
     return nullptr;
   }
+  const bool ignore_focus_state_for_testing =
+      base::CommandLine::ForCurrentProcess()->HasSwitch(
+          switches::kDisablePopupFocusedWindowDetectionForTesting);
+  if (!web_contents_has_focus_ && !ignore_focus_state_for_testing) {
+    // Don't create popups for non-focused tabs. http://crbug.com/365089001
+    return nullptr;
+  }
 
   RenderWidgetHostImpl* widget_host = RenderWidgetHostFactory::CreateSelfOwned(
       &primary_frame_tree_, this, site_instance_group, route_id, IsHidden());
@@ -6314,6 +6321,7 @@
     RenderWidgetHost* render_widget_host) {
   OPTIONAL_TRACE_EVENT1("content", "WebContentsImpl::NotifyWebContentsFocused",
                         "render_widget_host", render_widget_host);
+  web_contents_has_focus_ = true;
   observers_.NotifyObservers(&WebContentsObserver::OnWebContentsFocused,
                              render_widget_host);
 }
@@ -6323,6 +6331,7 @@
   OPTIONAL_TRACE_EVENT1("content",
                         "WebContentsImpl::NotifyWebContentsLostFocus",
                         "render_widget_host", render_widget_host);
+  web_contents_has_focus_ = false;
   observers_.NotifyObservers(&WebContentsObserver::OnWebContentsLostFocus,
                              render_widget_host);
 }
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 54a7f9578..881df057 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -2286,6 +2286,8 @@
   std::unique_ptr<ColorChooserHolder> color_chooser_holder_;
 #endif
 
+  bool web_contents_has_focus_{false};
+
   // All live RenderWidgetHostImpls that are created by this object and may
   // outlive it.
   std::set<raw_ptr<RenderWidgetHostImpl, SetExperimental>> created_widgets_;
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index 950f0b5..29f75ec 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -50,6 +50,7 @@
 #include "content/browser/renderer_host/text_input_manager.h"
 #include "content/browser/web_contents/web_contents_view.h"
 #include "content/common/content_navigation_policy.h"
+#include "content/common/content_switches_internal.h"
 #include "content/common/frame.mojom-test-utils.h"
 #include "content/common/frame.mojom.h"
 #include "content/public/browser/back_forward_cache.h"
@@ -6053,6 +6054,53 @@
   waiter.WaitForMediaDestroyed();
 }
 
+#if !BUILDFLAG(USE_EXTERNAL_POPUP_MENU)
+class SelectPickerBrowserTest : public WebContentsImplBrowserTest {
+ public:
+  SelectPickerBrowserTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        blink::features::kCSSPseudoOpenClosed);
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// Tests that popup pickers only open when the webcontents is focused.
+IN_PROC_BROWSER_TEST_F(SelectPickerBrowserTest, PickerOnlyOpensWhenFocused) {
+  base::CommandLine::ForCurrentProcess()->RemoveSwitch(
+      switches::kDisablePopupFocusedWindowDetectionForTesting);
+  ASSERT_FALSE(base::CommandLine::ForCurrentProcess()->HasSwitch(
+      switches::kDisablePopupFocusedWindowDetectionForTesting));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  GURL url(R"(data:text/html,<select><option>one</option></select>)");
+  ASSERT_TRUE(NavigateToURL(shell(), url));
+  WaitForLoadStop(web_contents);
+
+  // De-focus the window, which should keep popups from showing.
+  RenderWidgetHost* rwh =
+      web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget();
+  web_contents->NotifyWebContentsLostFocus(rwh);
+
+  // Open the picker - expect it not to open because window is not focused.
+  ASSERT_TRUE(
+      ExecJs(web_contents, "document.querySelector('select').showPicker();"));
+  EXPECT_EQ(false, EvalJs(web_contents,
+                          "document.querySelector('select').matches(':open')"));
+
+  // Now focus the window, which should enable popups.
+  web_contents->NotifyWebContentsFocused(rwh);
+
+  // Open the picker, and make sure it opened.
+  ASSERT_TRUE(
+      ExecJs(web_contents, "document.querySelector('select').showPicker();"));
+  EXPECT_EQ(true, EvalJs(web_contents,
+                         "document.querySelector('select').matches(':open')"));
+}
+#endif
+
 class WebContentsImplInsecureLocalhostBrowserTest
     : public WebContentsImplBrowserTest {
  protected:
diff --git a/content/common/content_switches_internal.cc b/content/common/content_switches_internal.cc
index dada3bf..0c461b5 100644
--- a/content/common/content_switches_internal.cc
+++ b/content/common/content_switches_internal.cc
@@ -37,6 +37,14 @@
 #include "base/win/windows_version.h"
 #endif
 
+namespace switches {
+// Disable popup focused-window detection. Done for tests to avoid
+// focus-dependent behavior.
+const char kDisablePopupFocusedWindowDetectionForTesting[] =
+    "disable-popup-focused-window-detection";
+
+}  // namespace switches
+
 namespace content {
 
 namespace {
diff --git a/content/common/content_switches_internal.h b/content/common/content_switches_internal.h
index 1047b3f1..6a5e32564 100644
--- a/content/common/content_switches_internal.h
+++ b/content/common/content_switches_internal.h
@@ -13,6 +13,11 @@
 class CommandLine;
 }
 
+namespace switches {
+CONTENT_EXPORT extern const char
+    kDisablePopupFocusedWindowDetectionForTesting[];
+}
+
 namespace content {
 
 blink::mojom::V8CacheOptions GetV8CacheOptions();
diff --git a/content/public/test/browser_test_base.cc b/content/public/test/browser_test_base.cc
index e297693..4f5787a 100644
--- a/content/public/test/browser_test_base.cc
+++ b/content/public/test/browser_test_base.cc
@@ -56,6 +56,7 @@
 #include "content/browser/tracing/memory_instrumentation_util.h"
 #include "content/browser/tracing/startup_tracing_controller.h"
 #include "content/browser/tracing/tracing_controller_impl.h"
+#include "content/common/content_switches_internal.h"
 #include "content/public/app/content_main.h"
 #include "content/public/app/initialize_mojo_core.h"
 #include "content/public/browser/browser_main_parts.h"
@@ -376,6 +377,11 @@
   command_line->AppendSwitch(
       switches::kDisableBackgroundingOccludedWindowsForTesting);
 
+  // The focused state of windows is unpredictable during tests, so disable
+  // focused window detection for popups when running browser tests.
+  command_line->AppendSwitch(
+      switches::kDisablePopupFocusedWindowDetectionForTesting);
+
   if (enable_pixel_output_) {
     DCHECK(!command_line->HasSwitch(switches::kForceDeviceScaleFactor))
         << "--force-device-scale-factor flag already present. Tests using "
diff --git a/content/renderer/render_thread_impl.cc b/content/renderer/render_thread_impl.cc
index e7db0f8a..5ac9504 100644
--- a/content/renderer/render_thread_impl.cc
+++ b/content/renderer/render_thread_impl.cc
@@ -541,7 +541,7 @@
 
 #if BUILDFLAG(USE_EXTERNAL_POPUP_MENU)
   // On Mac and Android Java UI, the select popups are rendered by the browser.
-    blink::WebView::SetUseExternalPopupMenus(true);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index 950f0b5..29f75ec 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -50,6 +50,7 @@
 #include "content/browser/renderer_host/text_input_manager.h"
 #include "content/browser/web_contents/web_contents_view.h"
 #include "content/common/content_navigation_policy.h"
+#include "content/common/content_switches_internal.h"
 #include "content/common/frame.mojom-test-utils.h"
 #include "content/common/frame.mojom.h"
 #include "content/public/browser/back_forward_cache.h"
@@ -6053,6 +6054,53 @@
   waiter.WaitForMediaDestroyed();
 }
 
+#if !BUILDFLAG(USE_EXTERNAL_POPUP_MENU)
+class SelectPickerBrowserTest : public WebContentsImplBrowserTest {
+ public:
+  SelectPickerBrowserTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        blink::features::kCSSPseudoOpenClosed);
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// Tests that popup pickers only open when the webcontents is focused.
+IN_PROC_BROWSER_TEST_F(SelectPickerBrowserTest, PickerOnlyOpensWhenFocused) {
+  base::CommandLine::ForCurrentProcess()->RemoveSwitch(
+      switches::kDisablePopupFocusedWindowDetectionForTesting);
+  ASSERT_FALSE(base::CommandLine::ForCurrentProcess()->HasSwitch(
+      switches::kDisablePopupFocusedWindowDetectionForTesting));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  GURL url(R"(data:text/html,<select><option>one</option></select>)");
+  ASSERT_TRUE(NavigateToURL(shell(), url));
+  WaitForLoadStop(web_contents);
+
+  // De-focus the window, which should keep popups from showing.
+  RenderWidgetHost* rwh =
+      web_contents->GetPrimaryMainFrame()->GetRenderViewHost()->GetWidget();
+  web_contents->NotifyWebContentsLostFocus(rwh);
+
+  // Open the picker - expect it not to open because window is not focused.
+  ASSERT_TRUE(
+      ExecJs(web_contents, "document.querySelector('select').showPicker();"));
+  EXPECT_EQ(false, EvalJs(web_contents,
+                          "document.querySelector('select').matches(':open')"));
+
+  // Now focus the window, which should enable popups.
+  web_contents->NotifyWebContentsFocused(rwh);
+
+  // Open the picker, and make sure it opened.
+  ASSERT_TRUE(
+      ExecJs(web_contents, "document.querySelector('select').showPicker();"));
+  EXPECT_EQ(true, EvalJs(web_contents,
+                         "document.querySelector('select').matches(':open')"));
+}
+#endif
+
 class WebContentsImplInsecureLocalhostBrowserTest
     : public WebContentsImplBrowserTest {
  protected:
diff --git a/content/public/test/browser_test_base.cc b/content/public/test/browser_test_base.cc
index e297693..4f5787a 100644
--- a/content/public/test/browser_test_base.cc
+++ b/content/public/test/browser_test_base.cc
@@ -56,6 +56,7 @@
 #include "content/browser/tracing/memory_instrumentation_util.h"
 #include "content/browser/tracing/startup_tracing_controller.h"
 #include "content/browser/tracing/tracing_controller_impl.h"
+#include "content/common/content_switches_internal.h"
 #include "content/public/app/content_main.h"
 #include "content/public/app/initialize_mojo_core.h"
 #include "content/public/browser/browser_main_parts.h"
@@ -376,6 +377,11 @@
   command_line->AppendSwitch(
       switches::kDisableBackgroundingOccludedWindowsForTesting);
 
+  // The focused state of windows is unpredictable during tests, so disable
+  // focused window detection for popups when running browser tests.
+  command_line->AppendSwitch(
+      switches::kDisablePopupFocusedWindowDetectionForTesting);
+
   if (enable_pixel_output_) {
     DCHECK(!command_line->HasSwitch(switches::kForceDeviceScaleFactor))
         << "--force-device-scale-factor flag already present. Tests using "
diff --git a/third_party/blink/web_tests/inspector-protocol/emulation/select-popup-auto-dark-mode.js b/third_party/blink/web_tests/inspector-protocol/emulation/select-popup-auto-dark-mode.js
index 5bd9964..5be48713 100644
--- a/third_party/blink/web_tests/inspector-protocol/emulation/select-popup-auto-dark-mode.js
+++ b/third_party/blink/web_tests/inspector-protocol/emulation/select-popup-auto-dark-mode.js
@@ -23,6 +23,7 @@
   testRunner.log("=== Before auto dark mode (autoDarkMode and prefers-color-scheme override) is not enabled ===");
   await session.evaluateAsync(`openPicker(document.querySelector("select"))`);
   await logScreenshotData();
+  await session.evaluateAsync(`attemptToClosePicker(document.querySelector("select"))`);
 
   await dp.Emulation.setAutoDarkModeOverride({enabled: true});
   await dp.Emulation.setEmulatedMedia({
@@ -36,6 +37,7 @@
   testRunner.log("\n=== After auto dark mode (autoDarkMode and prefers-color-scheme override) is enabled ===");
   await session.evaluateAsync(`openPicker(document.querySelector("select"))`);
   await logScreenshotData();
+  await session.evaluateAsync(`attemptToClosePicker(document.querySelector("select"))`);
 
   testRunner.completeTest();
 });
Loading diff…

Original Bug Report

reported by [email protected]

Select Option can be opened on top of Popups with Different Origins and can be used to Spoof important Security Prompts

SUMMARY

The Select option can be opened on popup with a different origin than the one that initiated it, which is simialr to the issue documented at https://issues.chromium.org/issues/41494315 (POC-1).

This issue also demonstrates different exploitation methods similar to https://issues.chromium.org/issues/342194497, where the permission prompt UI can be concealed (POC-2 and POC-3).

VULNERABILITY DETAILS

A malicious website can open a Select option on top of another website with an arbitrary origin via a popup, including trusted domains like google.com.

This allows the malicious website to display any text within the Select option on top of the opened popup.

The issue can be further exploited by opening a different permission or security prompt in this popup and hiding it with the Select option.

Below I have demonstrated this in three proof-of-concept files:

  1. poc-1.html: Demonstrates that the Select option can appear on top of an origin like google.com.

  2. poc-2.html and poc-3.html: Shows how the Select option can be used to cover the permission/security UI opened using a popup from a different website.

VERSION

  • Chrome Version:

    • 130.0.6701.0 (Official Build) Canary (64-bit)
    • 128.0.6613.120 (Official Build) (64-bit) Stable
  • Operating System: Windows 11

REPRODUCTION CASE

POC-1:

  1. Download the attached poc-1.html file.

  2. Host the page on a local server or open poc-1.html directly from the folder.

  3. Visit the webpage using the latest Chrome browser.

  4. Double-click on the website.

  5. Observe that the Select option appears over a different origin opened as a popup.

POC-2:

  1. Download the attached poc-2.html and permission.html files.

  2. Host the page on a local server or open poc-2.html directly from the folder.

  3. Visit the webpage using the latest Chrome browser.

  4. Double-click on the website.

  5. The Select option will request you to press Tab three times, then Enter to continue. Doing so will allow the hidden permission tab to proceed.

POC-3:

  1. Download the attached poc-3.html and permission.html files.

  2. Host the page on a local server or open poc-3.html directly from the folder.

  3. Visit the webpage using the latest Chrome browser.

  4. Double-click on the website.

  5. The permission prompt from the popup window is concealed by the Select option, thus spoofing the user interface.

Observed:

The Select option can open on top of a different website via a popup, rather than being on the initiated page, thus allowing malicious websites to spoof other sites or security prompts. This can be exploited to obtain permissions without the user’s awareness.

Expected:

The Select option should not open on top of popups. It should strictly be tied to the opened website, and when popup is focused chrome should not allow the Select option to be placed on top of it.

CREDIT INFORMATION

Reporter credit: Shaheen Fazim

View on issue tracker