CVE-2026-106246
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc |
modified | |
IN_PROC_BROWSER_TEST_Pchrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.ccchrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.ccchrome/browser/ui/exclusive_access/fullscreen_controller.h
Patch
From c3bfffdf1a5a09d2e1893eef51508b1cbd89d494 Mon Sep 17 00:00:00 2001 From: Thomas Lukaszewicz <[email protected]> Date: Thu, 10 Sep 2026 10:17:39 -0700 Subject: [PATCH] Use requesting origin for automatic fullscreen in modal dialogs When a tab is blocked by a modal dialog, content fullscreen mode is exited unless the automatic fullscreen content setting is granted. Previously, BrowserWindowModalDialogDelegate::SetWebContentsBlocked evaluated this setting using the top-level WebContents URL, which checked the primary page origin rather than the specific frame origin that initiated fullscreen. This CL exposes the requesting frame origin on FullscreenController and updates BrowserWindowModalDialogDelegate to query the automatic fullscreen setting against that origin, ensuring it is non-opaque before evaluating the permission. TAG=agy CONV=95848c56-47d8-4e65-9ea7-5d0b3d57c6b1 Fixed: 514070956 Change-Id: I4d84e4e2cb71fd7219686c892f2f7831efcbff8b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8384657 Commit-Queue: Thomas Lukaszewicz <[email protected]> Reviewed-by: David Yeung <[email protected]> Cr-Commit-Position: refs/heads/main@{#1695483} --- diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller.h b/chrome/browser/ui/exclusive_access/fullscreen_controller.h index 902872e..35b8dac 100644 --- a/chrome/browser/ui/exclusive_access/fullscreen_controller.h +++ b/chrome/browser/ui/exclusive_access/fullscreen_controller.h @@ -117,6 +117,10 @@ content::FullscreenState GetFullscreenState( const content::WebContents* web_contents) const; + // Returns the origin of the frame currently in tab-initiated fullscreen. + // Returns an opaque origin if not in tab-initiated fullscreen. + const url::Origin& requesting_origin() const { return requesting_origin_; } + // Returns true if |web_contents| is in fullscreen mode as a screen-captured // tab. See 'FullscreenWithinTab Note'. bool IsFullscreenWithinTab(const content::WebContents* web_contents) const; diff --git a/chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc b/chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc index bfcb028..6ef238d 100644 --- a/chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc +++ b/chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc @@ -22,7 +22,9 @@ #include "components/web_modal/web_contents_modal_dialog_host.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_frame_host.h" #include "content/public/browser/web_contents.h" +#include "url/origin.h" DEFINE_USER_DATA(BrowserWindowModalDialogDelegate); @@ -61,19 +63,24 @@ // Skip browser-fullscreen, which is more expressly user-initiated. // Skip fullscreen-within-tab, which shows the browser frame. if (blocked) { + FullscreenController* const fullscreen_controller = + ExclusiveAccessManager::From(browser_)->fullscreen_controller(); content::FullscreenState fullscreen_state = - ExclusiveAccessManager::From(browser_) - ->fullscreen_controller() - ->GetFullscreenState(web_contents); + fullscreen_controller->GetFullscreenState(web_contents); if (fullscreen_state.target_mode == content::FullscreenMode::kContent) { - // Skip URLs with the automatic fullscreen content setting granted. - const GURL& url = web_contents->GetLastCommittedURL(); + // Skip origins with the automatic fullscreen content setting granted. + const url::Origin& requesting_origin = + fullscreen_controller->requesting_origin(); + const GURL url = requesting_origin.GetURL(); const HostContentSettingsMap* const content_settings = HostContentSettingsMapFactory::GetForProfile( web_contents->GetBrowserContext()); - if (content_settings->GetContentSetting( + if (requesting_origin.opaque() || + requesting_origin != + web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin() || + content_settings->GetContentSetting( url, url, ContentSettingsType::AUTOMATIC_FULLSCREEN) != - CONTENT_SETTING_ALLOW) { + CONTENT_SETTING_ALLOW) { // Defer exiting fullscreen to prevent synchronous window management // messages (e.g. direct WndProc calls on Windows) from destroying the // WebContents or callers while modal dialog presentation is on the diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc index 28e9884b..00dc22d 100644 --- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc +++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc @@ -1321,6 +1321,67 @@ EXPECT_TRUE(web_contents_->IsFullscreen()); } +IN_PROC_BROWSER_TEST_P(AutomaticFullscreenTest, + BlockingContentsExitsForCrossOriginFrame) { +#if BUILDFLAG(IS_MAC) + if (GetParam()) { + GTEST_SKIP() << "Flaky. See https://crbug.com/404887514"; + } +#endif + + // Append a cross-origin iframe with the fullscreen permission policy. + const GURL src = embedded_https_test_server().GetURL("b.com", "/simple.html"); + content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame(); + web_app::CreateIframe(rfh, "", src, /*permissions_policy=*/"fullscreen *"); + content::RenderFrameHost* child = ChildFrameAt(rfh, 0); + + EXPECT_TRUE(RequestFullscreen(/*gesture=*/false, child)); + EXPECT_TRUE(web_contents_->IsFullscreen()); + + // Blocking the tab for a modal dialog exits fullscreen when a cross-origin + // iframe requested fullscreen, even if the top-level origin has the + // automatic fullscreen content setting granted. + BrowserWindowInterface* browser = + GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents_); + ui_test_utils::FullscreenWaiter waiter(browser, {.tab_fullscreen = false}); + BrowserWindowModalDialogDelegate::From(browser)->SetWebContentsBlocked( + web_contents_, true); + waiter.Wait(); + EXPECT_FALSE(web_contents_->IsFullscreen()); +} + +IN_PROC_BROWSER_TEST_P(AutomaticFullscreenTest, + BlockingContentsDoesNotExitForSameOriginFrame) { +#if BUILDFLAG(IS_MAC) + if (GetParam()) { + GTEST_SKIP() << "Flaky. See https://crbug.com/404887514"; + } +#endif + + if (GetParam()) { + // Isolated Web Apps in this test fixture do not serve subframe HTML files. + return; + } + + // Append a same-origin iframe with the fullscreen permission policy. + const GURL src = embedded_https_test_server().GetURL("a.com", "/simple.html"); + content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame(); + web_app::CreateIframe(rfh, "", src, /*permissions_policy=*/"fullscreen *"); + content::RenderFrameHost* child = ChildFrameAt(rfh, 0); + + EXPECT_TRUE(RequestFullscreen(/*gesture=*/false, child)); + EXPECT_TRUE(web_contents_->IsFullscreen()); + + // Blocking the tab for a modal dialog preserves fullscreen for a same-origin + // iframe when the origin has the automatic fullscreen content setting + // granted. + BrowserWindowInterface* browser = + GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents_); + BrowserWindowModalDialogDelegate::From(browser)->SetWebContentsBlocked( + web_contents_, true); + EXPECT_TRUE(web_contents_->IsFullscreen()); +} + IN_PROC_BROWSER_TEST_P(AutomaticFullscreenTest, QueryPermissionWithGesture) { // Expect an API TypeError when allowWithoutGesture is false or unspecified. EXPECT_EQ( @@ -1367,7 +1428,7 @@ web_app::CreateIframe(rfh, "", src, /*permissions_policy=*/"fullscreen *"); content::RenderFrameHost* child = ChildFrameAt(rfh, 0); EXPECT_EQ("granted", QueryPermission(child)); - EXPECT_TRUE(RequestFullscreen(child)); + EXPECT_TRUE(RequestFullscreen(/*gesture=*/false, child)); EXPECT_TRUE(ExitFullscreen()); }
Regression Test / PoC
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
index 28e9884b..00dc22d 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -1321,6 +1321,67 @@
EXPECT_TRUE(web_contents_->IsFullscreen());
}
+IN_PROC_BROWSER_TEST_P(AutomaticFullscreenTest,
+ BlockingContentsExitsForCrossOriginFrame) {
+#if BUILDFLAG(IS_MAC)
+ if (GetParam()) {
+ GTEST_SKIP() << "Flaky. See https://crbug.com/404887514";
+ }
+#endif
+
+ // Append a cross-origin iframe with the fullscreen permission policy.
+ const GURL src = embedded_https_test_server().GetURL("b.com", "/simple.html");
+ content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame();
+ web_app::CreateIframe(rfh, "", src, /*permissions_policy=*/"fullscreen *");
+ content::RenderFrameHost* child = ChildFrameAt(rfh, 0);
+
+ EXPECT_TRUE(RequestFullscreen(/*gesture=*/false, child));
+ EXPECT_TRUE(web_contents_->IsFullscreen());
+
+ // Blocking the tab for a modal dialog exits fullscreen when a cross-origin
+ // iframe requested fullscreen, even if the top-level origin has the
+ // automatic fullscreen content setting granted.
+ BrowserWindowInterface* browser =
+ GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents_);
+ ui_test_utils::FullscreenWaiter waiter(browser, {.tab_fullscreen = false});
+ BrowserWindowModalDialogDelegate::From(browser)->SetWebContentsBlocked(
+ web_contents_, true);
+ waiter.Wait();
+ EXPECT_FALSE(web_contents_->IsFullscreen());
+}
+
+IN_PROC_BROWSER_TEST_P(AutomaticFullscreenTest,
+ BlockingContentsDoesNotExitForSameOriginFrame) {
+#if BUILDFLAG(IS_MAC)
+ if (GetParam()) {
+ GTEST_SKIP() << "Flaky. See https://crbug.com/404887514";
+ }
+#endif
+
+ if (GetParam()) {
+ // Isolated Web Apps in this test fixture do not serve subframe HTML files.
+ return;
+ }
+
+ // Append a same-origin iframe with the fullscreen permission policy.
+ const GURL src = embedded_https_test_server().GetURL("a.com", "/simple.html");
+ content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame();
+ web_app::CreateIframe(rfh, "", src, /*permissions_policy=*/"fullscreen *");
+ content::RenderFrameHost* child = ChildFrameAt(rfh, 0);
+
+ EXPECT_TRUE(RequestFullscreen(/*gesture=*/false, child));
+ EXPECT_TRUE(web_contents_->IsFullscreen());
+
+ // Blocking the tab for a modal dialog preserves fullscreen for a same-origin
+ // iframe when the origin has the automatic fullscreen content setting
+ // granted.
+ BrowserWindowInterface* browser =
+ GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents_);
+ BrowserWindowModalDialogDelegate::From(browser)->SetWebContentsBlocked(
+ web_contents_, true);
+ EXPECT_TRUE(web_contents_->IsFullscreen());
+}
+
IN_PROC_BROWSER_TEST_P(AutomaticFullscreenTest, QueryPermissionWithGesture) {
// Expect an API TypeError when allowWithoutGesture is false or unspecified.
EXPECT_EQ(
@@ -1367,7 +1428,7 @@
web_app::CreateIframe(rfh, "", src, /*permissions_policy=*/"fullscreen *");
content::RenderFrameHost* child = ChildFrameAt(rfh, 0);
EXPECT_EQ("granted", QueryPermission(child));
- EXPECT_TRUE(RequestFullscreen(child));
+ EXPECT_TRUE(RequestFullscreen(/*gesture=*/false, child));
EXPECT_TRUE(ExitFullscreen());
}
Original Bug Report
Fullscreen persistence during modal dialogs via incorrect origin check
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential logic error in BrowserWindowModalDialogDelegate incorrectly uses the top-level frame’s URL to check for the AUTOMATIC_FULLSCREEN permission when a tab-modal dialog is displayed. This allows cross-origin iframes with fullscreen delegation to bypass the mandatory fullscreen exit intended to reveal the omnibox. Consequently, an attacker can maintain control over the entire screen to spoof the browser environment around legitimate dialogs.
Affected files:
chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc
Estimated timestamp from git blame: 2024-03-14
Potential Vulnerability: Incorrect origin check for AUTOMATIC_FULLSCREEN exemption
When a tab-modal dialog (such as a PaymentRequest, WebAuthn prompt, or HTTP authentication dialog) is displayed, Chrome is designed to force the page out of HTML fullscreen mode. This is a critical security measure that ensures the omnibox (address bar) is revealed, providing the user with the necessary security context and site identity before they interact with the dialog.
Root Cause
A logic error exists in the implementation of the AUTOMATIC_FULLSCREEN exemption within BrowserWindowModalDialogDelegate::SetWebContentsBlocked (located in chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc).
When determining whether to skip the mandatory fullscreen exit, the code retrieves the AUTOMATIC_FULLSCREEN content setting using the top-level WebContents URL instead of the origin of the frame that is actually in fullscreen:
// chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.cc
if (fullscreen_state.target_mode == content::FullscreenMode::kContent) {
// Potential Error: Retrieves top-level URL instead of the fullscreen frame's origin
const GURL& url = web_contents->GetLastCommittedURL();
const HostContentSettingsMap* const content_settings =
HostContentSettingsMapFactory::GetForProfile(
web_contents->GetBrowserContext());
if (content_settings->GetContentSetting(
url, url, ContentSettingsType::AUTOMATIC_FULLSCREEN) !=
CONTENT_SETTING_ALLOW) {
web_contents->ExitFullscreen(true);
}
}
Because AUTOMATIC_FULLSCREEN is a TOP_ORIGIN_ONLY_SCOPE content setting, this lookup will return ALLOW whenever the top-level origin has been granted the permission, regardless of which frame holds the fullscreen state.
Potential Impact
If a trusted top-level origin (e.g., an Isolated Web App or a site with a manual permission grant) has the AUTOMATIC_FULLSCREEN permission, an embedded cross-origin iframe with fullscreen delegation (allow="fullscreen") can exploit this to remain in fullscreen while a modal dialog is shown. This allows the attacker to spoof the entire browser UI (fake address bar, fake security indicators) around a legitimate browser-rendered dialog, which can facilitate phishing or credential theft.
Potential Steps to Reproduce
- Grant the “Automatic Fullscreen” permission to
https://trusted.example(e.g., viachrome://settings/content/automaticFullScreen). - Host a page at
https://trusted.example/index.htmlthat embeds an attacker-controlled iframe:<iframe src="https://attacker.example/" allow="fullscreen; payment"></iframe>. - As the attacker, have the iframe enter HTML fullscreen mode.
- Inside the iframe, trigger a tab-modal dialog, such as
new PaymentRequest(methods, details).show(). - Observation: The iframe remains in fullscreen mode, and the omnibox is not revealed when the PaymentRequest dialog appears.
Suggested Fix
The check should validate the origin of the frame currently in fullscreen. The browser should query the FullscreenController for the requesting_origin of the current fullscreen state rather than relying on the WebContents top-level URL.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.