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 45fc4ca6af569..a4344365b5a63 100644 --- a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc +++ b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc @@ -826,6 +826,18 @@ ContentSettingStorageAccessBubbleModel::ContentSettingStorageAccessBubbleModel( set_manage_text(l10n_util::GetStringUTF16(IDS_STORAGE_ACCESS_MANAGE_TEXT)); set_manage_tooltip( l10n_util::GetStringUTF16(IDS_STORAGE_ACCESS_MANAGE_TOOLTIP)); + + // [POC INJECTION] Log what the user actually sees, then simulate the user + // clicking the toggle for each row to ALLOW (the user does NOT press Done). + // None of this changes the vulnerable code path -- it only stands in for two + // mouse clicks. + LOG(ERROR) << "[POC] StorageAccess bubble constructed; user-visible " + "embedding origin (subtitle) = " + << web_contents->GetURL(); + for (const auto& [site, allowed] : bubble_content().site_list) { + LOG(ERROR) << "[POC] auto-toggling row to ALLOW for primary=" << site; + OnSiteRowClicked(site, /*is_allowed=*/true); + } } ContentSettingStorageAccessBubbleModel:: @@ -841,6 +853,13 @@ void ContentSettingStorageAccessBubbleModel::CommitChanges() { const GURL& secondary = web_contents()->GetURL(); ContentSetting setting = entry.second ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK; + // [POC LOGGING] Show the (primary, secondary) pair actually written to + // HostContentSettingsMap. `secondary` is read live from the WebContents + // *after* navigation has committed -- this is the bug. + LOG(ERROR) << "[POC] CommitChanges(): writing STORAGE_ACCESS grant primary=" + << primary << " secondary=" << secondary + << " setting=" << setting + << " (user saw subtitle for victim.test)"; permissions::PermissionUmaUtil::ScopedRevocationReporter scoped_revocation_reporter( GetProfile(), primary, secondary, @@ -853,6 +872,19 @@ void ContentSettingStorageAccessBubbleModel::CommitChanges() { map->SetNarrowestContentSetting(primary, secondary, ContentSettingsType::STORAGE_ACCESS, setting, constraints); + + // [POC LOGGING] Read back what was persisted: (idp.test, evil.test) and + // (idp.test, victim.test). 1 = ALLOW, 2 = BLOCK, 3 = ASK. + LOG(ERROR) << "[POC] readback: GetContentSetting(idp.test, victim.test, " + "STORAGE_ACCESS) = " + << map->GetContentSetting(GURL("http://idp.test/"), + GURL("http://victim.test/"), + ContentSettingsType::STORAGE_ACCESS); + LOG(ERROR) << "[POC] readback: GetContentSetting(idp.test, evil.test, " + "STORAGE_ACCESS) = " + << map->GetContentSetting(GURL("http://idp.test/"), + GURL("http://evil.test/"), + ContentSettingsType::STORAGE_ACCESS); } } diff --git a/chrome/browser/ui/views/content_setting_bubble_contents.cc b/chrome/browser/ui/views/content_setting_bubble_contents.cc index 41e787a97dfbf..47a6b2e05d8f4 100644 --- a/chrome/browser/ui/views/content_setting_bubble_contents.cc +++ b/chrome/browser/ui/views/content_setting_bubble_contents.cc @@ -322,6 +322,9 @@ ContentSettingBubbleContents::~ContentSettingBubbleContents() { } void ContentSettingBubbleContents::WindowClosing() { + // [POC LOGGING] reached via Widget::HandleWidgetDestroying after the + // posted CloseNow task ran. + LOG(ERROR) << "[POC] WindowClosing -> CommitChanges()"; if (content_setting_bubble_model_) { content_setting_bubble_model_->CommitChanges(); } @@ -608,6 +611,12 @@ void ContentSettingBubbleContents::CustomLinkClicked() { void ContentSettingBubbleContents::PrimaryPageChanged(content::Page& page) { // Content settings are based on the main frame, so if it switches then // close up shop. + // [POC LOGGING] navigation committed while the bubble is open. By the time + // this fires, NavigationController already returns the *new* URL. + LOG(ERROR) << "[POC] PrimaryPageChanged fired while bubble open; " + "web_contents()->GetURL() now = " + << web_contents()->GetURL() + << " -- calling Widget::Close() (async)."; GetWidget()->Close(); } diff --git a/chrome/browser/ui/views/location_bar/content_setting_image_view.cc b/chrome/browser/ui/views/location_bar/content_setting_image_view.cc index 95a34e3f97522..78f84b180c7c1 100644 --- a/chrome/browser/ui/views/location_bar/content_setting_image_view.cc +++ b/chrome/browser/ui/views/location_bar/content_setting_image_view.cc @@ -22,6 +22,12 @@ #include "chrome/browser/ui/color/chrome_color_id.h" #include "chrome/browser/ui/content_settings/content_setting_bubble_model.h" #include "chrome/browser/ui/content_settings/content_setting_image_model.h" +// [POC INJECTION] extra includes for seeding the Storage Access two-site +// request without driving the full requestStorageAccess()+prompt flow. +#include "components/content_settings/browser/page_specific_content_settings.h" +#include "components/content_settings/core/common/content_settings.h" +#include "content/public/browser/web_contents.h" +#include "net/base/schemeful_site.h" #include "chrome/browser/ui/user_education/browser_user_education_interface.h" #include "chrome/browser/ui/view_ids.h" #include "chrome/browser/ui/views/content_setting_bubble_contents.h" @@ -150,6 +156,27 @@ void ContentSettingImageView::Update() { content::WebContents* web_contents = delegate_->GetContentSettingWebContents(); + // [POC INJECTION] Seed PageSpecificContentSettings with a blocked + // STORAGE_ACCESS two-site request (idp.test embedded in victim.test). In a + // real attack this is produced by the idp.test iframe calling + // document.requestStorageAccess() and the user dismissing the prompt -- + // see StorageAccessGrantPermissionContext::NotifyPermissionSetInternal, + // which calls exactly this method. Seeding it here avoids driving an + // interactive permission prompt under ozone-headless and changes nothing + // about the vulnerable CommitChanges() path. + if (web_contents && + content_setting_image_model_->image_type() == + ContentSettingImageModel::ImageType::STORAGE_ACCESS && + web_contents->GetURL().host() == "victim.test") { + if (auto* pscs = + content_settings::PageSpecificContentSettings::GetForFrame( + web_contents->GetPrimaryMainFrame())) { + pscs->OnTwoSitePermissionChanged( + ContentSettingsType::STORAGE_ACCESS, + net::SchemefulSite(GURL("http://idp.test/")), CONTENT_SETTING_BLOCK); + } + } + bool force_hide = delegate_->ShouldHideContentSettingImage(); content_setting_image_model_->Update(force_hide ? nullptr : web_contents); SetTooltipText(content_setting_image_model_->get_tooltip()); @@ -182,6 +209,26 @@ void ContentSettingImageView::Update() { content_setting_image_model_->SetBubbleWasAutoOpened(web_contents); } + // [POC INJECTION] Simulate the user clicking the Storage Access omnibox + // indicator as soon as it appears. In a real attack the user clicks the + // indicator (so the bubble has focus and stays open); pinning + // close_on_deactivate=false reproduces that with ozone-headless where the + // synthetic bubble never receives activation. + static bool s_poc_bubble_opened = false; + if (content_setting_image_model_->image_type() == + ContentSettingImageModel::ImageType::STORAGE_ACCESS && + !bubble_view_ && !s_poc_bubble_opened && + !web_contents->IsLoading() && + web_contents->GetURL().host() == "victim.test") { + s_poc_bubble_opened = true; + LOG(ERROR) << "[POC] auto-opening Storage Access bubble for " + << web_contents->GetURL(); + ShowBubbleImpl(); + if (bubble_view_) { + bubble_view_->set_close_on_deactivate(false); + } + } + // If the content usage or blockage should be indicated to the user, start the // animation and record that the icon has been shown. if (!can_animate_ ||