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 0000000000..1111111111 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 @@ -1506,6 +1506,55 @@ TEST_F(ContentSettingBubbleModelTest, StorageAccess) { map->GetContentSetting(site.GetURL(), web_contents()->GetURL(), ContentSettingsType::STORAGE_ACCESS)); } + +// Regression demo: CommitChanges() reads web_contents()->GetURL() live, so a +// navigation that commits between bubble construction (what the user saw) and +// WindowClosing()->CommitChanges() writes the STORAGE_ACCESS grant for the +// *post-navigation* embedding origin instead of the one the user approved. +TEST_F(ContentSettingBubbleModelTest, + StorageAccessGrantKeyedToPostNavigationOrigin) { + const GURL victim("https://victim.test/"); + const GURL evil("https://evil.test/"); + const net::SchemefulSite idp(GURL("https://idp.test")); + auto* map = HostContentSettingsMapFactory::GetForProfile(profile()); + + // 1. User is on victim.test; an embedded idp.test iframe was denied + // storage access -> indicator appears. + WebContentsTester::For(web_contents())->NavigateAndCommit(victim); + PageSpecificContentSettings::GetForFrame( + web_contents()->GetPrimaryMainFrame()) + ->OnTwoSitePermissionChanged(ContentSettingsType::STORAGE_ACCESS, idp, + CONTENT_SETTING_BLOCK); + + // 2. User clicks the indicator: bubble model is constructed and snapshots + // "victim.test" into the subtitle. + std::unique_ptr model( + ContentSettingBubbleModel::CreateContentSettingBubbleModel( + nullptr, web_contents(), ContentSettingsType::STORAGE_ACCESS)); + EXPECT_NE(std::u16string::npos, + model->bubble_content().subtitle.find(u"victim.test")); + + // 3. User toggles the row for idp.test to ALLOW (does not press Done). + model->OnSiteRowClicked(idp, /*is_allowed=*/true); + + // 4. Attacker navigates the top-level frame to evil.test while the bubble + // is still open. PrimaryPageChanged() then calls Widget::Close() which + // asynchronously runs WindowClosing()->CommitChanges(). + WebContentsTester::For(web_contents())->NavigateAndCommit(evil); + model->CommitChanges(); + + // BUG: the grant is written for (idp, evil) -- the live-read origin -- + // and *not* for (idp, victim) which is what the user saw and approved. + EXPECT_EQ(CONTENT_SETTING_ALLOW, + map->GetContentSetting(idp.GetURL(), evil, + ContentSettingsType::STORAGE_ACCESS)) + << "forged grant written for evil.test"; + EXPECT_NE(CONTENT_SETTING_ALLOW, + map->GetContentSetting(idp.GetURL(), victim, + ContentSettingsType::STORAGE_ACCESS)) + << "intended grant for victim.test was silently lost"; +} + #if BUILDFLAG(IS_CHROMEOS) TEST_F(ContentSettingBubbleModelTest, SmartCard) {