CVE-2026-87641
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
content_type_chrome/browser/ui/content_settings/content_setting_bubble_model.cc |
modified | |
ContentSettingMixedScriptBubbleModelchrome/browser/ui/content_settings/content_setting_bubble_model.cc |
modified |
Files Changed
chrome/browser/chrome_back_forward_cache_browsertest.ccchrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.ccchrome/browser/ui/content_settings/BUILD.gnchrome/browser/ui/content_settings/content_setting_bubble_model.cc
Patch
From abc3ca95bb5e737e118c75b966afb8e74a63d59e Mon Sep 17 00:00:00 2001 From: Antonio Sartori <[email protected]> Date: Tue, 28 Jul 2026 05:01:15 -0700 Subject: [PATCH] Adjust lifecycle of ContentSettingBubbleModel to strongly match a Page This CL updates ContentSettingBubbleModel to store base::SafeRef<content::Page> instead of WebContents*. This ensures that the model always has a valid reference to the page it is associated with, and makes logical bugs that could be triggered by the ContentSettingBubbleModel surviving a cross-document navigation basically impossible. The only small functional effect of this change is that bubbles that are not closed by the user (but instead automatically closed because of a navigation or a tab being closed) will not commit changes anymore. Before this change, this depended on whether the WebContents died (changes committed) or navigated away (changes not committed), which seemed confusing. Bug: 523091391 Change-Id: I4f8609e88723359d05e92e8844d97875a5563a53 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7953760 Reviewed-by: Arthur Sonzogni <[email protected]> Reviewed-by: Balazs Engedy <[email protected]> Reviewed-by: Mike Wasserman <[email protected]> Commit-Queue: Antonio Sartori <[email protected]> Cr-Commit-Position: refs/heads/main@{#1669366} --- diff --git a/chrome/browser/chrome_back_forward_cache_browsertest.cc b/chrome/browser/chrome_back_forward_cache_browsertest.cc index a690b19..9b2ea491 100644 --- a/chrome/browser/chrome_back_forward_cache_browsertest.cc +++ b/chrome/browser/chrome_back_forward_cache_browsertest.cc @@ -385,12 +385,18 @@ // to run. content::TestNavigationObserver observer( browser()->tab_strip_model()->GetActiveWebContents()); - std::unique_ptr<ContentSettingBubbleModel> model( - ContentSettingBubbleModel::CreateContentSettingBubbleModel( - browser()->GetFeatures().content_setting_bubble_model_delegate(), - browser()->tab_strip_model()->GetActiveWebContents(), - ContentSettingsType::MIXEDSCRIPT)); - model->OnCustomLinkClicked(); + + { + std::unique_ptr<ContentSettingBubbleModel> model( + ContentSettingBubbleModel::CreateContentSettingBubbleModel( + browser()->GetFeatures().content_setting_bubble_model_delegate(), + browser() + ->tab_strip_model() + ->GetActiveWebContents() + ->GetPrimaryPage(), + ContentSettingsType::MIXEDSCRIPT)); + model->OnCustomLinkClicked(); + } // 3) Wait for reload. observer.Wait(); diff --git a/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc b/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc index 2827a61..f0e71b38 100644 --- a/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc +++ b/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc @@ -72,11 +72,14 @@ // Emulates link clicking on the mixed script bubble to allow mixed content // to run. content::TestNavigationObserver observer(web_contents()); - std::unique_ptr<ContentSettingBubbleModel> model( - ContentSettingBubbleModel::CreateContentSettingBubbleModel( - browser()->GetFeatures().content_setting_bubble_model_delegate(), - web_contents(), ContentSettingsType::MIXEDSCRIPT)); - model->OnCustomLinkClicked(); + { + std::unique_ptr<ContentSettingBubbleModel> model( + ContentSettingBubbleModel::CreateContentSettingBubbleModel( + browser()->GetFeatures().content_setting_bubble_model_delegate(), + web_contents()->GetPrimaryPage(), + ContentSettingsType::MIXEDSCRIPT)); + model->OnCustomLinkClicked(); + } // Waits for reload. observer.Wait(); @@ -146,12 +149,14 @@ // to run. content::TestNavigationObserver observer( browser()->tab_strip_model()->GetActiveWebContents()); - std::unique_ptr<ContentSettingBubbleModel> model( - ContentSettingBubbleModel::CreateContentSettingBubbleModel( - browser()->GetFeatures().content_setting_bubble_model_delegate(), - browser()->tab_strip_model()->GetActiveWebContents(), - ContentSettingsType::MIXEDSCRIPT)); - model->OnCustomLinkClicked(); + { + std::unique_ptr<ContentSettingBubbleModel> model( + ContentSettingBubbleModel::CreateContentSettingBubbleModel( + browser()->GetFeatures().content_setting_bubble_model_delegate(), + web_contents()->GetPrimaryPage(), + ContentSettingsType::MIXEDSCRIPT)); + model->OnCustomLinkClicked(); + } // Waits for reload. observer.Wait(); @@ -314,12 +319,15 @@ // to run. content::TestNavigationObserver observer( browser()->tab_strip_model()->GetActiveWebContents()); - std::unique_ptr<ContentSettingBubbleModel> model( - ContentSettingBubbleModel::CreateContentSettingBubbleModel( - browser()->GetFeatures().content_setting_bubble_model_delegate(), - browser()->tab_strip_model()->GetActiveWebContents(), - ContentSettingsType::MIXEDSCRIPT)); - model->OnCustomLinkClicked(); + + { + std::unique_ptr<ContentSettingBubbleModel> model( + ContentSettingBubbleModel::CreateContentSettingBubbleModel( + browser()->GetFeatures().content_setting_bubble_model_delegate(), + web_contents()->GetPrimaryPage(), + ContentSettingsType::MIXEDSCRIPT)); + model->OnCustomLinkClicked(); + } // Waits for reload. observer.Wait(); diff --git a/chrome/browser/ui/content_settings/BUILD.gn b/chrome/browser/ui/content_settings/BUILD.gn index 073e20e..c8ab64a 100644 --- a/chrome/browser/ui/content_settings/BUILD.gn +++ b/chrome/browser/ui/content_settings/BUILD.gn @@ -26,6 +26,7 @@ "content_setting_image_model.h", "content_setting_image_model_states.h", "content_setting_image_view_delegate.h", + "primary_page_deactivation_helper.h", ] public_deps += [ "//chrome/app/vector_icons" ] } @@ -40,6 +41,7 @@ "content_setting_bubble_model.cc", "content_setting_image_model.cc", "content_setting_image_model_states.cc", + "primary_page_deactivation_helper.cc", ] deps += [ "//chrome/browser/custom_handlers", @@ -103,6 +105,7 @@ "//chrome/test:test_support", "//components/content_settings/core/test:test_support", "//components/subresource_filter/core/browser", + "//ui/events:test_support", ] } diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc index b6c2cfc..fcb771e1 100644 --- a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc +++ b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc @@ -214,9 +214,11 @@ constexpr UrlIdentity::TypeSet allowed_types = { UrlIdentity::Type::kDefault, UrlIdentity::Type::kFile, UrlIdentity::Type::kIsolatedWebApp}; -constexpr UrlIdentity::FormatOptions options; -std::u16string GetUrlForDisplay(Profile* profile, const GURL& url) { +std::u16string GetUrlForDisplay( + Profile* profile, + const GURL& url, + const UrlIdentity::FormatOptions& options = UrlIdentity::FormatOptions()) { if (g_display_url_override_for_testing.value_or(false)) { return GetDefaultDisplayURLForTesting(); // IN-TEST } @@ -289,10 +291,9 @@ ContentSettingSimpleBubbleModel::ContentSettingSimpleBubbleModel( Delegate* delegate, - WebContents* web_contents, + content::Page& page, ContentSettingsType content_type) - : ContentSettingBubbleModel(delegate, web_contents), - content_type_(content_type) { + : ContentSettingBubbleModel(delegate, page), content_type_(content_type) { SetTitle(); SetMessage(); SetManageText(); @@ -306,11 +307,11 @@ bool ContentSettingSimpleBubbleModel::IsContentAllowed() { PageSpecificContentSettings* content_settings = - PageSpecificContentSettings::GetForFrame(&GetPage().GetMainDocument()); + PageSpecificContentSettings::GetForPage(GetPage()); if (content_type() == ContentSettingsType::COOKIES) { ContentSetting setting; - GetSettingManagedByUser(web_contents()->GetLastCommittedURL(), + GetSettingManagedByUser(GetPage().GetMainDocument().GetLastCommittedURL(), content_type(), GetProfile(), &setting); // We check the content setting here as well because 3PC access influences // the allowed/blocked status even though the icon is meant for 1PC control. @@ -455,8 +456,7 @@ class ContentSettingMixedScriptBubbleModel
Regression Test / PoC
diff --git a/chrome/browser/chrome_back_forward_cache_browsertest.cc b/chrome/browser/chrome_back_forward_cache_browsertest.cc
index a690b19..9b2ea491 100644
--- a/chrome/browser/chrome_back_forward_cache_browsertest.cc
+++ b/chrome/browser/chrome_back_forward_cache_browsertest.cc
@@ -385,12 +385,18 @@
// to run.
content::TestNavigationObserver observer(
browser()->tab_strip_model()->GetActiveWebContents());
- std::unique_ptr<ContentSettingBubbleModel> model(
- ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- browser()->GetFeatures().content_setting_bubble_model_delegate(),
- browser()->tab_strip_model()->GetActiveWebContents(),
- ContentSettingsType::MIXEDSCRIPT));
- model->OnCustomLinkClicked();
+
+ {
+ std::unique_ptr<ContentSettingBubbleModel> model(
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ browser()->GetFeatures().content_setting_bubble_model_delegate(),
+ browser()
+ ->tab_strip_model()
+ ->GetActiveWebContents()
+ ->GetPrimaryPage(),
+ ContentSettingsType::MIXEDSCRIPT));
+ model->OnCustomLinkClicked();
+ }
// 3) Wait for reload.
observer.Wait();
diff --git a/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc b/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc
index 2827a61..f0e71b38 100644
--- a/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc
+++ b/chrome/browser/content_settings/mixed_content_settings_tab_helper_browsertest.cc
@@ -72,11 +72,14 @@
// Emulates link clicking on the mixed script bubble to allow mixed content
// to run.
content::TestNavigationObserver observer(web_contents());
- std::unique_ptr<ContentSettingBubbleModel> model(
- ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- browser()->GetFeatures().content_setting_bubble_model_delegate(),
- web_contents(), ContentSettingsType::MIXEDSCRIPT));
- model->OnCustomLinkClicked();
+ {
+ std::unique_ptr<ContentSettingBubbleModel> model(
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ browser()->GetFeatures().content_setting_bubble_model_delegate(),
+ web_contents()->GetPrimaryPage(),
+ ContentSettingsType::MIXEDSCRIPT));
+ model->OnCustomLinkClicked();
+ }
// Waits for reload.
observer.Wait();
@@ -146,12 +149,14 @@
// to run.
content::TestNavigationObserver observer(
browser()->tab_strip_model()->GetActiveWebContents());
- std::unique_ptr<ContentSettingBubbleModel> model(
- ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- browser()->GetFeatures().content_setting_bubble_model_delegate(),
- browser()->tab_strip_model()->GetActiveWebContents(),
- ContentSettingsType::MIXEDSCRIPT));
- model->OnCustomLinkClicked();
+ {
+ std::unique_ptr<ContentSettingBubbleModel> model(
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ browser()->GetFeatures().content_setting_bubble_model_delegate(),
+ web_contents()->GetPrimaryPage(),
+ ContentSettingsType::MIXEDSCRIPT));
+ model->OnCustomLinkClicked();
+ }
// Waits for reload.
observer.Wait();
@@ -314,12 +319,15 @@
// to run.
content::TestNavigationObserver observer(
browser()->tab_strip_model()->GetActiveWebContents());
- std::unique_ptr<ContentSettingBubbleModel> model(
- ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- browser()->GetFeatures().content_setting_bubble_model_delegate(),
- browser()->tab_strip_model()->GetActiveWebContents(),
- ContentSettingsType::MIXEDSCRIPT));
- model->OnCustomLinkClicked();
+
+ {
+ std::unique_ptr<ContentSettingBubbleModel> model(
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ browser()->GetFeatures().content_setting_bubble_model_delegate(),
+ web_contents()->GetPrimaryPage(),
+ ContentSettingsType::MIXEDSCRIPT));
+ model->OnCustomLinkClicked();
+ }
// Waits for reload.
observer.Wait();
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_browsertest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_browsertest.cc
index 4c48aa2..6cd61e8 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model_browsertest.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_browsertest.cc
@@ -75,7 +75,7 @@
state);
return std::make_unique<ContentSettingMediaStreamBubbleModel>(
browser()->GetFeatures().content_setting_bubble_model_delegate(),
- web_contents);
+ web_contents->GetPrimaryPage());
}
content::WebContents* GetActiveTab() {
@@ -185,7 +185,7 @@
std::unique_ptr<ContentSettingBubbleModel> mic_bubble =
std::make_unique<ContentSettingMediaStreamBubbleModel>(
browser()->GetFeatures().content_setting_bubble_model_delegate(),
- web_contents);
+ web_contents->GetPrimaryPage());
EXPECT_TRUE(mic_bubble->bubble_content().is_user_modifiable);
}
@@ -344,7 +344,10 @@
std::unique_ptr<ContentSettingBubbleModel> model(
ContentSettingBubbleModel::CreateContentSettingBubbleModel(
browser()->GetFeatures().content_setting_bubble_model_delegate(),
- browser()->tab_strip_model()->GetActiveWebContents(),
+ browser()
+ ->tab_strip_model()
+ ->GetActiveWebContents()
+ ->GetPrimaryPage(),
ContentSettingsType::POPUPS));
std::unique_ptr<FakeOwner> owner =
FakeOwner::Create(*model, kDisallowButtonIndex);
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
index 9bba1d9..1bda534 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
@@ -96,6 +96,7 @@
web_contents());
}
+ content::Page& page() { return web_contents()->GetPrimaryPage(); }
TestingProfile::TestingFactories GetTestingFactories() const override {
return {TestingProfile::TestingFactory{
HistoryServiceFactory::GetInstance(),
@@ -113,7 +114,7 @@
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- nullptr, web_contents(), ContentSettingsType::IMAGES));
+ nullptr, page(), ContentSettingsType::IMAGES));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_FALSE(bubble_content.title.empty());
@@ -165,7 +166,7 @@
VerifyBubbleContent(
CONTENT_SETTING_BLOCK,
ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- nullptr, web_contents(), ContentSettingsType::COOKIES)
+ nullptr, page(), ContentSettingsType::COOKIES)
->bubble_content());
}
@@ -191,7 +192,7 @@
content_settings->OnContentAllowed(ContentSettingsType::COOKIES);
VerifyBubbleContent(
site_setting, ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- nullptr, web_contents(), ContentSettingsType::COOKIES)
+ nullptr, page(), ContentSettingsType::COOKIES)
->bubble_content());
// Even if cookies are allowed on the 1P site, it's still possible for
@@ -200,7 +201,7 @@
content_settings->OnContentBlocked(ContentSettingsType::COOKIES);
VerifyBubbleContent(
site_setting, ContentSettingBubbleModel::CreateContentSettingBubbleModel(
- nullptr, web_contents(), ContentSettingsType::COOKIES)
+ nullptr, page(), ContentSettingsType::COOKIES)
->bubble_content());
}
@@ -237,7 +238,7 @@
microphone_camera_state);
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(bubble_content.title,
@@ -287,7 +288,7 @@
content_settings->OnMediaStreamPermissionSet(url, microphone_camera_state);
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
// Test if the correct radio item is selected for the blocked mediastream
@@ -344,7 +345,7 @@
content_settings->OnMediaStreamPermissionSet(url, microphone_camera_state);
{
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
// Test if the correct radio item is selected for the blocked mediastream
@@ -363,7 +364,7 @@
{
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
// Test that the reload hint is displayed.
@@ -386,7 +387,7 @@
{
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
// Test that the reload hint is not displayed any more.
@@ -427,7 +428,7 @@
microphone_camera_state);
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(bubble_content.title,
@@ -451,8 +452,7 @@
content_settings->OnMediaStreamPermissionSet(security_origin,
microphone_camera_state);
content_setting_bubble_model =
- std::make_unique<ContentSettingMediaStreamBubbleModel>(nullptr,
- web_contents());
+ std::make_unique<ContentSettingMediaStreamBubbleModel>(nullptr, page());
const ContentSettingBubbleModel::BubbleContent& new_bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(new_bubble_content.title,
@@ -502,7 +502,7 @@
microphone_camera_state);
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(bubble_content.title,
@@ -526,8 +526,7 @@
content_settings->OnMediaStreamPermissionSet(security_origin,
microphone_camera_state);
content_setting_bubble_model =
- std::make_unique<ContentSettingMediaStreamBubbleModel>(nullptr,
- web_contents());
+ std::make_unique<ContentSettingMediaStreamBubbleModel>(nullptr, page());
const ContentSettingBubbleModel::BubbleContent& new_bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(new_bubble_content.title,
@@ -582,7 +581,7 @@
microphone_camera_state);
std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
- new ContentSettingMediaStreamBubbleModel(nullptr, web_contents()));
+ new ContentSettingMediaStreamBubbleModel(nullptr, page()));
const ContentSettingBubbleModel::BubbleContent& bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(bubble_content.title,
@@ -604,8 +603,7 @@
microphone_camera_state);
content_setting_bubble_model =
- std::make_unique<ContentSettingMediaStreamBubbleModel>(nullptr,
- web_contents());
+ std::make_unique<ContentSettingMediaStreamBubbleModel>(nullptr, page());
const ContentSettingBubbleModel::BubbleContent& new_bubble_content =
content_setting_bubble_model->bubble_content();
EXPECT_EQ(new_bubble_content.title,
@@ -679,8 +677,7 @@
// System-level geolocation permission is blocked.
if (is_os_level_geolocation_permission_support_enabled) {
auto content_setting_bubble_model =
- std::make_unique<ContentSettingGeolocationBubbleModel>(nullptr,
- web_contents());
+ std::make_unique<ContentSettingGeolocationBubbleModel>(nullptr, page());
std::unique_ptr<FakeOwner> owner =
FakeOwner::Create(*content_setting_bubble_model, 0);
const auto& bubble_content = content_setting_bubble_model->bubble_content();
@@ -700,8 +697,7 @@
// bubble is visible. The displayed message should not change.
if (is_os_level_geolocation_permission_support_enabled) {
auto content_setting_bubble_model =
- std::make_unique<ContentSettingGeolocationBubbleModel>(nullptr,
- web_contents());
+ std::make_unique<ContentSettingGeolocationBubbleModel>(nullptr, page());
std::unique_ptr<FakeOwner> owner =
FakeOwner::Create(*content_setting_bubble_model, 0);
const auto& bubble_content = content_setting_bubble_model->bubble_content();
... (truncated)
Original Bug Report
Potential Permission Revocation Bypass in ContentSettingMediaStreamBubble
Flapjack, 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 logic error in the media stream permission bubble allows a site to prevent a user from revoking camera or microphone access. If the site triggers a cross-document navigation while the bubble is open, the browser’s revocation logic executes against the new page state, silently discarding the user’s explicit intent to block permissions.
Affected files:
chrome/browser/ui/content_settings/content_setting_bubble_model.cc
Estimated timestamp from git blame: 2023-07-19
Summary
The ContentSettingMediaStreamBubbleModel manages the omnibox permission bubble for Camera and Microphone access. A potential vulnerability exists because the user’s decision to “Block” access is not applied immediately; it is deferred until the bubble closes via CommitChanges(). However, CommitChanges() fetches the page state using GetPage().GetMainDocument(), which refers to the current primary page of the associated WebContents.
If a cross-document navigation commits while the bubble is open, the WebContentsObserver::PrimaryPageChanged event fires, causing the bubble to close synchronously. During the destruction sequence, CommitChanges() is invoked, but GetPage() now points to the newly committed page. Because the new page has not yet requested media streams, its PageSpecificContentSettings has an empty media_stream_access_origin. This causes CommitChanges() to hit an early return, entirely skipping the code that updates the user’s content settings.
Potential Attack Scenario
An attacker who has already been granted Camera/Microphone access could use this to maintain persistent access despite explicit user intervention to revoke it. (Note: These are suggested steps based on static analysis; our tooling cannot run live code).
- The user grants Camera/Mic permissions to
https://attacker.com. - The malicious page listens for the
window.onblurevent. When the user clicks the omnibox media indicator, the permission bubble steals focus, triggering the event. - The malicious page reacts to
onblurby initiating a delayed cross-document navigation (e.g.,location.href = '/delayed_response'). - The user clicks the radio button to “Block” the camera/microphone.
- Before the user clicks “Done” or closes the bubble manually, the delayed navigation commits.
- The browser invokes
WebContentsImpl::DidCommitNavigationand updates the primary page. WebContentsObserver::PrimaryPageChangedis fired.ContentSettingBubbleContentsreceives this and callsGetWidget()->Close()to dismiss the bubble since the page changed.- The widget destruction sequence calls
ContentSettingBubbleContents::WindowClosing(), which invokescontent_setting_bubble_model_->CommitChanges(). CommitChanges()callsPageSpecificContentSettings::GetForFrame(&GetPage().GetMainDocument()). This retrieves the settings for the new page.- The code evaluates
if (content_settings->media_stream_access_origin().is_empty()). Since the new page hasn’t accessed media, this is true. - The function returns early.
UpdateSettings()is never called, and the user’s intent to revoke the permission is silently discarded.
Code Reference
In chrome/browser/ui/content_settings/content_setting_bubble_model.cc:
void ContentSettingMediaStreamBubbleModel::CommitChanges() {
PageSpecificContentSettings* content_settings =
PageSpecificContentSettings::GetForFrame(&GetPage().GetMainDocument());
// ...
if (content_settings->media_stream_access_origin().is_empty()) {
return; // <--- Early return hit because of the new page state
}
// ...
if (!ShouldShowSystemMediaPermissions()) {
if (selected_item() != bubble_content().radio_group.default_item) {
UpdateSettings(radio_item_setting_[selected_item()]); // <--- Revocation is skipped
}
}
}
Suggested Fix
Instead of querying PageSpecificContentSettings::GetForFrame(&GetPage().GetMainDocument()) during CommitChanges(), the ContentSettingMediaStreamBubbleModel should cache the media_stream_access_origin (or the specific url to be modified) when the model is constructed. This ensures that the user’s action is applied to the origin that was actively using the media stream when the bubble was opened, regardless of subsequent navigations.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.